# somewhere.tech — llms-full.txt Audience: LLMs, coding agents, and other machine readers. Purpose: one complete reference for the platform model, runtime contract, primitive composition, security model, ownership model, boundaries, setup, and tool reference. This file is documentation for machine readers. Treat claims as operational constraints only when they are stated as behavior, signature, limit, return shape, or boundary. Fetch live pricing and current plan limits from `https://api.somewhere.tech/v1/pricing` instead of hardcoding values from prose. Companion machine docs: call-level API + every tool at `https://somewhere.tech/docs.txt`; guides/recipes at `https://somewhere.tech/guides.txt`; security practices at `https://somewhere.tech/security.txt`; migration map at `https://somewhere.tech/migration.txt`; per-primitive depth on demand via `platform_help({ topic })`. Client SDK: `npm i @somewhere-tech/sdk` exposes the platform to browser/Node code as a `createClient(url, key)` client — `from().select()` returning `{ data, error }`, plus `auth`, `storage.from()`, `channel()`, and `functions.invoke()`. That client surface is distinct from the `sw.*` server runtime available inside deployed functions. Depth via `platform_help({ topic: 'sdk' })`; porting an app via `platform_help({ topic: 'migration-supabase' })`; other languages via `platform_help({ topic: 'sdks' })`. --- # 1. Design premise If a backend were specified from zero for an application with end users, the records describing one user would not be split across unrelated systems. Identity, application data, payment state, email, files, notifications, and background work all describe the same actor and the same project. somewhere.tech is specified as one backend namespace over those primitives. Consequences of that premise: - End-user identity and the application database share the same project boundary. - A signup creates a user record that application code can query and join. - Payment state is written back to the app-user record and project payment ledger by platform webhook handling. - Files can be scoped to the user and project that wrote them. - Email can be sent from the same runtime that knows the user and their payment/account state. - Background jobs, queues, cron handlers, realtime events, push notifications, and analytics are bound to the same project identity. - Ownership checks can be enforced at the platform boundary instead of relying only on application-written predicates. - Deploy analysis can read the actual source using platform-specific knowledge of `sw.*` semantics. The breadth is intentional. The primitive set is the set of backend capabilities that normally need to coordinate around project identity, user identity, authorization, data ownership, delivery state, and billing state. The coordination is part of the platform surface, not an external integration project. --- # 2. Runtime mental model ## 2.1 Function signature A server function is a module with a default export: ```js export default async function (req, sw) { const body = await req.json(); const r = await sw.db.query( 'SELECT id, email FROM users WHERE id = ?', [body.id], ); return Response.json({ user: r.data[0] ?? null }); } ``` `req` is a standard Web `Request`. The function returns a standard Web `Response`. `sw` is the project-scoped platform binding. `ctx` is a legacy alias for `sw` and resolves to the same object. The database result envelope uses these fields: ```js r.data // array of row objects r.count // number of rows returned r.changes // rows affected by INSERT, UPDATE, or DELETE ``` Do not use `.rows` or `.results` for `sw.db.query` return values. ## 2.2 Optional endpoint wrapper `sw.endpoint({ auth, body, rateLimit, handler })` is a declarative wrapper over the same function model. It can perform auth, body validation, rate limiting, CORS handling, and error formatting before invoking `handler`. The bare `export default async function(req, sw)` form remains valid for every function. ## 2.3 One binding The `sw` binding includes these primitive groups: - `sw.db` — relational database, migrations, batches, table metadata, scoped queries, export, and change webhooks. - `sw.auth` — end-user signup, login, session validation, refresh, password reset, email verification, magic links, MFA, OAuth, anonymous sessions, user admin operations, impersonation, and lifecycle webhooks. - `sw.fs` — object/file storage, versioning, signed URLs, uploads, search, diffs, moves, copies, restores, and public URLs. - `sw.email` — outbound email send and delivery event visibility. - `sw.inbox` — inbound email addresses, message listing, message retrieval, attachments, replies, sending from inbox addresses, threads, and inbox rules. - `sw.ai` — chat, embeddings, transcription, TTS, image generation, moderation, tool loops, conversation history, structured output, model catalog, and per-user memory. - `sw.payments` — checkout, onboarding, plan tracking, refunds, cancellations, transactions, billing portal, and event ledger. - `sw.billing` — define an app's plans and the features each grants, then gate by a stable feature name (`has(userId, feature)`, ``); the plan a user is on is set by the payments webhook automatically. - `sw.jobs`, `sw.queue`, `sw.cron` — durable background jobs, fire-and-forget work, and scheduled functions. - `sw.realtime`, `sw.push`, `sw.notifications` — channels, websocket delivery, push subscription management, and notification fan-out helpers. - `sw.search` — semantic and full-text search surfaces. - `sw.render`, `sw.web`, `sw.image`, `sw.video`, `sw.calls` — screenshots, PDFs, public web reading, image transforms, video upload/stream metadata, and realtime audio/video sessions. - `sw.rateLimit`, `sw.analytics`, `sw.logs`, `sw.env`, `sw.tasks` — rate-limit counters, event analytics, runtime logs, server-only environment variables, and per-project ticket/task state. ## 2.4 Internal and external access Inside a deployed function, use `sw.*`. The binding is already scoped to the project. No developer key is required inside runtime code. Outside a deployed function, use one of the external surfaces: - MCP tools at `https://mcp.somewhere.tech/mcp`. - CLI commands through the `somewhere` binary. - REST API paths under `/v1/*` with an appropriate developer key or app-user JWT. Do not call `https://api.somewhere.tech/v1/*` from inside a deployed function when an equivalent `sw.*` primitive exists. Runtime code should use the binding. ## 2.5 Deploy input Deploy raw source. Valid inputs include `.ts`, `.tsx`, `.js`, `.jsx`, `.html`, `.css`, static assets, and function files under `api/` or other documented function paths. The platform compiles and bundles where applicable at deploy time. Prebuilt output can be rejected with `BUNDLED_DEPLOY_REJECTED` according to the deploy reference. --- # 3. Connection and installation ## 3.1 MCP connector Connector URL: ```text https://mcp.somewhere.tech/mcp ``` An MCP host that supports remote connectors can attach this URL. Once authenticated, the agent receives project, deploy, database, filesystem, log, help, advisor, task, and other platform tools. ## 3.2 CLI Install and authenticate: ```sh npm i -g @somewhere-tech/cli somewhere auth login ``` The CLI writes the session to `~/.somewhere/config.json`. The CLI and MCP bridge use the same local configuration. Use `somewhere deploy` for file-system-backed deploys. Use MCP reads such as `db_query`, `project_export`, `project_file_read`, `fs_read`, and `log` when the agent needs in-context state. ## 3.3 Claude Code plugin Claude Code plugin commands: ```text /plugin marketplace add Somewhere-Tech/claude-code-plugin /plugin install somewhere-tech@somewhere-tech-claude-code-plugin ``` Reload Claude Code, then run: ```sh somewhere auth login ``` After plugin activation, `mcp__somewhere__*` tools are available in Claude Code. ## 3.4 Ephemeral environments In sandboxes or browser-hosted environments without persistent home directories, the browser login may not survive the session. Pair CLI auth to the authenticated MCP session: ```text 1. npm i -g @somewhere-tech/cli 2. call MCP tool auth_cli_pair() 3. somewhere auth set 4. somewhere whoami 5. somewhere deploy ``` The paired key is a short-lived CLI pairing key. It can be revoked independently. --- # 4. Primitive composition model ## 4.1 Database and auth `sw.auth.signup` creates an end-user account in the project identity system. User records are queryable application data. `sw.auth.fromRequest(req)` resolves the current user from a cookie or `Authorization: Bearer` header. `sw.db.query(sql, params, { user })` scopes declared user-owned tables to that user. For builder-style code, `sw.db.scoped(user.id).from(table)` provides list, insert, and update helpers that carry ownership by construction. The composition rule: resolve the user once, then pass that user or user id into database operations that touch user-owned state. ## 4.2 Payments and auth `sw.payments.checkout` creates checkout state. Payment webhook handling records event state and updates app-user plan fields when platform plan tracking is configured. Application code can check the authenticated user and their payment/plan state without reconciling separate identities. `sw.billing` is the entitlement layer on top: define plans and their features in code, then gate on a stable feature name with `sw.billing.has(userId, feature)` server-side or `` / `useEntitlements()` client-side (the feature list rides the session, so the client check needs no extra request). Gating on a feature name rather than a plan name or price means renaming or re-pricing a plan never touches gate code. The composition rule: price and checkout are server-side operations; app code reads plan state from the user/payment records after platform processing. ## 4.3 Files and auth `sw.fs` stores bytes under project state. Files are private by default unless public visibility or a public URL is explicitly used. Upload handlers can resolve the current user, place files under user-specific paths, and store file metadata in the database. The composition rule: file bytes live in `sw.fs`; ownership and application metadata live in `sw.db`; request authority comes from `sw.auth`. ## 4.4 Email, inbox, and auth `sw.email.send` sends outbound email from application code. Delivery events, bounces, and complaints are recorded by platform email surfaces. `sw.inbox` receives inbound mail for project-managed addresses and can reply or send from those addresses. Inbox messages can be connected to users, tickets, jobs, or AI handlers through database records and webhooks. The composition rule: use `sw.email` for outbound notifications; use `sw.inbox` for application-owned addresses and inbound workflows; use `sw.db` for durable application state derived from messages. ## 4.5 AI and data `sw.ai.chat` performs model calls. Conversation history can be persisted by passing a `conversation_id`. Embeddings and semantic search connect through `sw.search`. AI-generated actions can read/write durable state through `sw.db`, store artifacts through `sw.fs`, send mail through `sw.email`, and dispatch follow-up work through jobs or queues. The composition rule: model output is not the system of record; durable state belongs in `sw.db` or `sw.fs`. ## 4.6 Jobs, queue, and cron `sw.jobs` represents durable background work with status. `sw.queue` represents fire-and-forget message handling. `sw.cron` schedules repeated handlers. These surfaces run inside the same project context and can call the same `sw.*` primitives as request handlers. The composition rule: request handlers should enqueue work when latency, retries, or scheduled processing should not block the HTTP response. ## 4.7 Realtime, push, and notifications `sw.realtime` publishes message-shaped events to channels. Realtime has no durable replay guarantee; persistent state belongs in `sw.db`. `sw.push` manages web-push subscriptions and sends push payloads. `sw.notifications` provides a higher-level notification helper for user-targeted delivery. The composition rule: write state first, then publish or notify. Subscribers refetch state on reconnect when gaps matter. ## 4.8 Search `sw.fs.search` searches project file contents. `sw.search` and MCP search index tools manage semantic search indexes. Search results should reference durable records, files, or database ids rather than becoming the durable record themselves. ## 4.9 Render, web, image, video, and calls `sw.render` creates screenshots and PDFs. `sw.web` reads public web pages and search results. `sw.image` performs image transformations. Video and calls surfaces manage media upload/stream metadata and realtime audio/video sessions. These primitives compose with `sw.fs` for stored outputs, `sw.db` for metadata, `sw.auth` for access control, and jobs for long-running processing. ## 4.10 Logs, analytics, rate limiting, env, and tasks `sw.logs` records application logs. `sw.analytics` records event data and aggregate queries. `sw.rateLimit` provides atomic counters for endpoint limits. `sw.env` exposes server-only environment variables at runtime. `sw.tasks` stores per-project task and comment state in the project database. ## 4.11 Live data for many clients For a value many clients watch at once (live scores, a price ticker, a leaderboard), the platform composition is one server-side poller fanning out over realtime — never per-client polling. A scheduled function (cron) fetches the external source once per tick, writes a snapshot to `sw.db` or `sw.fs`, and publishes the change on a `sw.realtime` channel; each client reads the snapshot once for first paint, then subscribes for updates. Load on the external source is O(1) regardless of audience size, where per-client polling scales source load with the number of viewers. Cron's minimum interval is one minute (a 5-field schedule has no sub-minute granularity). For sub-minute cadence the platform pattern is to publish on `sw.realtime` the moment new data arrives — a webhook or inbound event — rather than scheduling; a cron poller at the one-minute floor refreshes the snapshot while realtime carries intra-minute changes. The composition rule: reference and seed data (rosters, catalogs, venue lists) is built into `sw.db` or a `sw.fs` JSON file by a re-runnable job — extracted from sources via `sw.ai` where needed — and read at runtime, never hardcoded in the frontend, so the data changes without a redeploy. --- # 5. Platform-owned responsibilities Application code should rely on the following platform-owned behavior and should not duplicate it unless a documented extension point requires explicit handling. ## 5.1 Runtime and upstream calls - `sw.*` calls are authenticated by the runtime binding. - Platform-owned upstream calls carry documented timeout, retry, and rate-limit behavior. - Error envelopes include `error`, `message`, `hint`, and when applicable `retry` and `retry_after_ms`. - Application-level nested retry loops around `sw.*` calls can amplify failures and should be avoided unless the specific method reference instructs otherwise. ## 5.2 Database consistency - `sw.db.batch` runs multiple statements in one transaction. - Project writes are serialized through a per-project write coordinator. - User-scoped table declarations let platform code enforce owner predicates. - Database dump/export surfaces return portable data formats. - Point-in-time recovery and backup behavior are platform responsibilities according to the database and security references. ## 5.3 Auth and tokens - Passwords are stored as PBKDF2-SHA256 hashes with per-password salt according to the security reference. - App-user JWTs are signed with per-project signing keys. - Password reset, magic link, refresh, and MFA token use is atomic. - Header-based refresh can return new access and refresh tokens without a client-side 401 retry loop. - Developer keys are for external developer/admin surfaces, not browser code and not runtime `sw.*` access. ## 5.4 Fetch safety - Platform-side fetches of user-controlled URLs use SSRF controls: DNS resolution checks, private/link-local/metadata IP blocks, redirect validation, timeout, and byte cap. - Surfaces include render, scrape, webhooks, transcription by URL, and other documented URL-fetching primitives. ## 5.5 Files - Uploaded files are private by default. - Public serving requires explicit public visibility or a public URL surface. - Writes use atomic metadata swaps so half-written files do not serve. - File versions and restore are separate from project code versions. ## 5.6 Email - Outbound email is sent through platform email infrastructure. - DKIM/SPF/DMARC and verified sender-domain handling are part of the email/domain model. - Bounce and complaint webhooks are recorded by the platform. - `email_bounces_list` surfaces recipients that should be scrubbed before send loops. ## 5.7 Deploys - Deploys are versioned. - `project_patch` modifies one file at a time and preserves other files. - `project_deploy` is a full replacement of the project tree. - `expected_version` prevents overwriting another deployer's changes. - Bundled output can be rejected. - Post-deploy outputs include screenshots, architecture diagram, application description, and optional/security-tier review surfaces. - Blank-page detection and scheduled smoke tests attach runtime health state to deploy versions. ## 5.8 Domains and certificates - Domain claiming, nameserver delegation, project attachment, certificate state, and inbox-routing state are represented in platform domain surfaces. - Inbox routing is opt-in because it changes MX behavior. ## 5.9 Billing and usage - Exact prices and limits are live data. Use `/v1/pricing`. - Payment processing uses the documented payments surface. - Card data is handled by the payment processor and does not pass through application code. - AI usage is drawn from balance or plan-specific allowance according to current pricing data. --- # 6. Security model ## 6.1 Philosophy One request resolves to one authority. A credential of one authority class cannot act as another authority class. Admin/configuration routes are not reachable by end-user or runtime credentials. Data routes are scoped to the caller. ## 6.2 Authority classes | Authority | Credential | Allowed scope | |---|---|---| | anonymous | no credential | static assets, public pre-login flows, signature-verified public webhooks | | app_user | per-project end-user JWT | own rows/files in the project through app-user-allowed paths; no raw SQL; no config | | runtime_project | deployed function binding | application primitives for one project; no deploy/env/key/billing/domain admin | | internal_signed | platform HMAC delivery | timestamped platform-internal delivery only | | developer_admin | developer session or `smt_` key | developer-owned project administration; audit-logged | ## 6.3 Isolation - Each project has its own database. - Runtime keys are project-bound. - Tenant code runs in isolated runtime units on edge compute. - Uploaded files are not publicly readable by path guessing. - Cross-project access returns not found or denial according to route policy. ## 6.4 End-user row ownership App-user credentials cannot run raw SQL, cannot enumerate tables, and cannot export/dump data. User-owned data access should go through scoped APIs. Declared user-scoped tables have platform-enforced owner predicates in supported query paths. Complex queries should include explicit ownership filters in function code and can be combined with platform scoping where applicable. ## 6.5 Authentication and transport - HTTPS is required for platform and deployed-app traffic. - Passwords use PBKDF2-SHA256 with 100,000 iterations and unique per-password salt, according to the security document. - App-user JWTs are signed with per-project secrets. - Developer dashboard sessions use HttpOnly cookies. - Developer API keys are stored hashed at rest. - MFA/TOTP is available for end-user accounts. ## 6.6 Deploy review and route policy verification The platform includes a deterministic scanner during compile and an AI security review surface for deeper review. The security program includes route-policy verification across authorities: anonymous, app_user, runtime, developer, and internal. A route that admits an unintended authority is a policy failure. ## 6.7 Compliance and boundaries - Not intended for HIPAA-regulated protected health information. - Card data is handled by the payment processor; application code should not handle raw card data. - The platform runs on third-party infrastructure and is not self-hostable. - Enterprise arrangements may define additional requirements through contract; this file does not imply those requirements by default. --- # 7. Ownership, export, and migration ## 7.1 Data export Database export surfaces: - `sw.db.dump()` from inside a function. - MCP `db_dump`. - REST `POST /v1/db/dump`. The dump is a SQL file containing schema and rows as SQL statements. For single-table CSV, use the documented table export surface. Large-table caps and streaming export availability are defined in the database reference. File export surfaces: - `sw.fs.read`, `sw.fs.list`, signed URLs, dashboard file browsing, and external REST/MCP file tools. - File bytes are stored as ordinary bytes, not as a proprietary application format. User export: - App-user accounts, profile data, metadata, and password hashes live in project data according to the auth/database model. - Password hashes use standard PBKDF2 parameters described in the security model. - Sessions are JWT-based and project-scoped. Source export: - `project_export` returns deployed source files and functions. - `somewhere pull` pulls the deployed source tree to disk. - Project code is plain JS/TS/HTML/CSS and deploy functions are plain modules using the documented function signature. ## 7.2 Incremental adoption and removal Each core primitive is also reachable through external MCP/CLI/REST surfaces. A project can use one primitive from an existing server, then add more primitives, or replace one primitive at a time. The migration operation is per primitive: | Platform surface | Generic replacement surface | |---|---| | `sw.db.query(sql, params)` | SQL client query call using the exported schema/data | | `sw.db.batch([...])` | SQL transaction | | `sw.db.migrate(sql)` | SQL migration tool or raw migration runner | | `sw.auth.signup/login/me/refresh` | auth/session service or application-owned JWT issue/verify | | `sw.email.send(...)` | transactional email sender SDK or SMTP-compatible sender | | `sw.inbox` | inbound email routing and webhook receiver | | `sw.fs.write/read/list/delete` | object storage SDK | | `sw.ai.*` | model-provider SDKs and application-managed conversation state where needed | | `sw.payments.*` | payment processor SDK and application-managed webhook ledger | | `sw.jobs`, `sw.queue`, `sw.cron` | queue, worker, and scheduler | | `sw.realtime` | websocket/pubsub service or application-managed websocket server | | `sw.search` | search index or vector-search system | | `sw.render` | browser automation service or hosted browser worker | | `sw.rateLimit` | counter store and rate-limit middleware | | `sw.analytics` | event store and aggregation pipeline | | `sw.push` | web-push library and VAPID key management | | `sw.video` | video ingest/streaming system | | `sw.calls` | realtime audio/video session system | | `sw.env.KEY` | host environment variables or secrets manager | Application logic usually keeps the same high-level shape: request handler, authenticated user resolution, database call, side-effect primitive, response. The replacement operation changes clients and responsibility boundaries. ## 7.3 Non-exported platform behavior What does not export as data is the work performed by the platform around the primitives: - ownership enforcement; - route authority policy; - deploy guardrails; - security review; - post-deploy screenshots, architecture, description, and blank-page checks; - email delivery event ingestion; - payment webhook ingestion and plan tracking; - token lifecycle enforcement; - SSRF-safe platform fetch paths; - per-project write serialization; - platform dashboards and usage accounting. Leaving the platform means those responsibilities must be implemented in the destination environment if the application still requires them. --- # 8. Boundaries - The platform is not self-hostable. - The platform is not intended for HIPAA-regulated PHI. - The runtime is a function/runtime binding model, not a raw server or operating-system model. - Deployed functions should not depend on local disk persistence. - Realtime is a fire-and-forget channel transport; durable history and replay belong in the database. - The database is a managed SQL database per project with documented SQL support and documented differences; specialized analytical or extension-heavy workloads may require a separate data system. If a project outgrows the default database, reach out — we'll work with you on a larger managed database. Most projects never need this. - Built on SOC 2-certified cloud, email, and payments infrastructure — the providers underneath maintain SOC 2. The platform is not pursuing its own SOC 2 attestation. - Render/browser automation has documented timeouts and input-size limits. - File storage versioning is separate from project source-code versioning. - Environment variable changes take effect on the next deploy of functions that read them. - The pricing endpoint is the source of truth for current tiers, limits, and rates. --- # 9. Reference: full tool and capability material The remaining sections preserve the per-topic reference material from the public documentation with prohibited sections removed. # Topic library ## sw.db — Database (inside deployed functions) (sw.db) Available inside any deployed function via the sw argument (ctx still works as an alias forever — same object, both names). sw.db talks to the project database through a direct binding — no HTTP, no API key, no network hop. Each project has its own isolated database attached to the deployed function bundle. Use `sw.db` inside deployed functions. Use the REST `/v1/db/query` surface only from outside the runtime, such as a local machine or another host. ## What you get out of the box (capability list) - **Multi-tenant isolation** — one database per project, bindings baked at deploy time, no shared schema. - **Row-Level Security (RLS)** — pass `{ user }` to `sw.db.query` and the platform server-side-rewrites your SQL to inject `WHERE owner_id = ?` from the JWT subject. See "Per-user scoping" below. - **ACID transactions** — `sw.db.batch([…])` runs all statements atomically. If statement 3 fails, 1 and 2 roll back. Same transaction semantics: all statements commit or all roll back. - **Common SQL syntax accepted** — `$1`, `$2` placeholders, `ILIKE`, `NOW()`, `TRUE`/`FALSE`, `col->>'key'`, `RETURNING *`, `SERIAL`, and `BOOLEAN` are translated automatically. Translation is literal- and comment-aware. - **Write coordinator** — concurrent writes to the same project are serialized through a per-project write coordinator, so database-busy errors never reach your handler. Reads run in parallel and are never blocked by writes. - **Point-in-time recovery (PITR)** — 30 days of time-travel from the underlying database, plus named bookmarks via `db_bookmark_create({ label })` for restore points around migrations. - **Schema introspection** — `db_describe` returns tables + columns + row counts + foreign keys in one call. - **Runaway-query detection** — a query that monopolizes database resources is detected and surfaced automatically. Full reviewer-facing depth: . ## sw.db.query(sql, params?, options?) Run a SQL query from inside a deployed function. Always returns this exact shape: { data: rows[], // array of row objects (READ + RETURNING) error: null, // never thrown; errors propagate as exceptions count: number, // rows.length last_row_id: number|null, // INSERT rowid (writes only) changes: number, // rows touched (INSERT/UPDATE/DELETE) } Use `r.data` for the rows. The MCP `db_query` tool returns a different shape (`{ data: { columns, rows, meta } }`) because it surfaces the raw platform response — don't confuse them. Inside functions it's always `r.data`. // Read const users = await sw.db.query( 'SELECT id, email FROM users WHERE active = ? ORDER BY created_at DESC LIMIT ?', [1, 20] ) // users.data = [{ id: 1, email: "alice@test.com" }, ...] // users.count = users.data.length // Write const result = await sw.db.query( 'INSERT INTO users (email, name) VALUES (?, ?) RETURNING *', ['bob@test.com', 'Bob'] ) // result.data = [{ id: 3, email: "bob@test.com", name: "Bob" }] // result.last_row_id = 3, result.changes = 1 ## Per-user scoping — pass { user } and we filter for you Tables declared as user-scoped (see "sw.db.scoped" section) are protected against accidental cross-user reads. The platform refuses to run a raw query against them unless you tell it whose data you want — that way one forgotten `WHERE owner_id = ?` can't leak another user's rows. The simple, recommended pattern: const me = sw.auth.fromRequest(req).user; const emails = await sw.db.query( 'SELECT * FROM emails ORDER BY created_at DESC LIMIT 20', [], { user: me }, ); The platform sees `emails` is scoped, sees you've bound `me`, and rewrites the query to `… WHERE owner_id = ?` with `me.id` filled in. You never wrote a WHERE clause. You can't forget it. `{ user }` accepts the user object from `sw.auth.fromRequest` OR a plain string user id — `{ user: 'usr_…' }` works too. Auto-scoping covers SELECT / UPDATE / DELETE / single-row INSERT on a SINGLE scoped table. For an **app-user** query that JOINs, comma-joins, nests (subquery / CTE), or UNIONs AND touches a user-scoped table, the platform **fails closed** — it returns a `SCOPE_VIOLATION` rather than risk reading another user's rows (a multi-table owner filter can't be attributed automatically yet). What to do instead: - Query each scoped table on its own (single-table, with its ` = $current_user` filter, or via the structured query API), then combine the results in your function. - Or run the read in **developer / runtime** mode (your function's key), where there is no app-user scoping — and enforce ownership yourself with an explicit `WHERE owner_id = ?`. (Roadmap: automatic per-table scoping for common JOINs. Until then, complex app-user queries on scoped tables fail closed rather than leak.) ### { unscoped: true } — opt out of safety Some queries are intentionally cross-user — analytics, admin dashboards, public reads. Pass `{ unscoped: true }` and the platform stops checking: // Admin endpoint: see every project's email count. const allCounts = await sw.db.query( 'SELECT owner_id, COUNT(*) AS n FROM emails GROUP BY owner_id', [], { unscoped: true }, ); The security advisor will flag `{ unscoped: true }` so reviewers know it was a deliberate decision, not a forgotten predicate. Use it sparingly and only where cross-user reads are the whole point. ## Placeholders: ? (positional) or $1, $2, ... (numbered) Both styles work — pick one per statement, don't mix them. // Numbered placeholders — params are reordered to match the $N indexes. // Useful when the same value appears in multiple positions. await sw.db.query( 'UPDATE users SET name = $2, updated_by = $2 WHERE id = $1', ['u_123', 'Alice'] ) // Mixing ? and $N in one statement is rejected with VALIDATION_ERROR. ## sw.db.batch(statements) Run multiple statements as one atomic transaction. If ANY statement fails, ALL roll back. All-or-nothing. const results = await sw.db.batch([ { sql: 'DELETE FROM sessions WHERE user_id = ?', params: ['u_123'] }, { sql: 'DELETE FROM posts WHERE author_id = ?', params: ['u_123'] }, { sql: 'DELETE FROM users WHERE id = ?', params: ['u_123'] } ]) // results = [{ data: [], changes: 3 }, { data: [], changes: 12 }, { data: [], changes: 1 }] // If the third DELETE failed, the first two would also roll back. ## sw.db.migrate(sql) Run DDL statements (CREATE TABLE, ALTER TABLE, CREATE INDEX). Multiple statements separated by semicolons. await sw.db.migrate(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, name TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); `) ## sw.db.tables() List all tables in the database. Returns string[]. const tables = await sw.db.tables() // ['users', 'posts', 'sessions'] ## SQL dialect Standard SQL. Common SQL dialect conveniences are auto-translated: NOW() → datetime('now') TRUE/FALSE → 1/0 ILIKE → LIKE (case-insensitive by default) SERIAL → INTEGER PRIMARY KEY AUTOINCREMENT BOOLEAN → INTEGER RETURNING * → supported ## Common patterns // Paginated list const page = await sw.db.query( 'SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?', [pageSize, (pageNum - 1) * pageSize] ) // Count const total = await sw.db.query('SELECT COUNT(*) as count FROM posts') // total.data[0].count = 42 // Upsert await sw.db.query( 'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?', ['theme', 'dark', 'dark'] ) // Join const postsWithAuthors = await sw.db.query(` SELECT p.*, u.name as author_name FROM posts p JOIN users u ON p.author_id = u.id WHERE p.published = 1 ORDER BY p.created_at DESC `) ## Performance Queries are fast (~1-50ms). For static reference data that rarely changes, baking it into your function bundle as a JSON constant avoids the round-trip entirely. ## Change webhooks — sw.db.onchange Get notified after every successful INSERT / UPDATE / UPSERT / DELETE. One webhook per project. Payload is small and predictable; no row contents are sent, so your webhook handler can't accidentally leak sensitive data: { project_id, table, op, rows_affected, ts } Register / read / remove from inside a function: const reg = await sw.db.onchange.set('https://example.com/db-hook', { events: ['insert', 'update', 'delete'], // optional; default = all }); // reg.secret → save this if you didn't already; it's how you verify // the signature on incoming POSTs. await sw.db.onchange.get(); await sw.db.onchange.delete(); Every POST carries: X-Somewhere-Signature: t={ms},v1={hmac-sha256-hex} where the signed string is `${ms}.${rawBody}`. Verify with a constant-time compare and reject requests where `Math.abs(Date.now() - ms)` is over your replay tolerance (5 minutes is typical). The webhook fires from waitUntil after the write commits, so your handler's latency never blocks the developer's query. There is no automatic retry — keep your endpoint idempotent and fast. The registration row tracks last_status / last_error so you can see the most recent delivery outcome via sw.db.onchange.get(). Equivalent REST surface (developer key only): PUT /v1/db/webhook { project_id, url, events? } GET /v1/db/webhook?project_id=… DELETE /v1/db/webhook?project_id=… --- ## Deployed Functions (functions) Server-side code that runs on the platform. Deploy via project_deploy. File path = route path. api/hello.ts → /api/hello ## Compilation: ship .ts, not pre-built .mjs Functions go through the same deploy-time bundler the frontend uses. You write `api/chat.ts` (with whatever imports it needs); the platform produces a self-contained `.mjs` the runtime executes. **Do not pre-bundle.** No `esbuild api/chat.ts -o api/chat.mjs`, no `tsc`, no `vite build` — `/v1/deploy` HARD-REJECTS pre-built output (`BUNDLED_DEPLOY_REJECTED`). What works inside a function source file — **all natively, no restructuring**: - TypeScript syntax (type annotations, interfaces) — stripped. - Type-only imports (`import { type X } from './types'`) — stripped. - Imports from ANY folder, not just `_lib/` — `import { foo } from '../utils/helpers'`, `import { z } from './schemas'`, etc. - **No extension required** — `'./helper'` resolves to `./helper.ts`/`.tsx`/`.js`/`.mjs`/`.json` automatically. - **JSON imports** — `import data from './seed.json'` inlines the JSON as a native object. - Workers runtime built-ins — `import x from 'cloudflare:workers'`, `import crypto from 'node:crypto'` pass through. **npm packages work** — `import { z } from 'zod'`, `import Stripe from 'stripe'`, and any other npm package import resolve automatically at deploy. Pin versions in your `package.json` (e.g. `zod@^3.22`); unpinned imports get the latest. No build step on your side — you deploy raw source and the platform handles it. ## Format Every function file has one default export: // api/hello.ts export default async function(req, sw) { return Response.json({ hello: 'world' }) } req = standard Request object (method, headers, json(), text(), etc.) sw = platform context with all services (ctx is the legacy alias) ## sw.endpoint — auth + validation + rate-limit (usually what you want) The bare `export default async function(req, sw)` above always works, but most endpoints need auth, body validation, or a rate limit. `sw.endpoint` folds all of that in so you only write business logic. It is optional — reach for it when you want auth, validation, and rate-limiting handled for you; the bare function form above always works too: // api/signup.ts export default sw.endpoint({ auth: 'none', // 'required' | 'optional' | 'none' body: { email: 'email', password: 'string' }, // validated → 400 on mismatch rateLimit: '5/minute', // → 429 + Retry-After handler: async ({ body, user }, sw) => { const u = await sw.auth.signup(body.email, body.password); return { ok: true, user_id: u.id }; // plain object → Response.json }, }); Full reference (schema types, handler args { body, user, headers, params, request }, error formatting, CORS): `platform_help({ topic: 'sw.endpoint' })`. ## sw services available sw.db — database (query, batch, migrate, tables) sw.fs — file storage (read, write, delete, move, stat, list, glob, diff, search, replace) sw.email — send email (send) sw.ai — AI models (complete, embed, transcribe, tts, generateImage, removeBackground, catalog) sw.env — environment variables (sw.env.STRIPE_KEY etc.) sw.jobs — background jobs (create) sw.queue — fire-and-forget work (push) sw.logs — application logging (write) ## Routing File path becomes the URL path: api/hello.ts → /api/hello api/users/list.ts → /api/users/list api/auth/login.ts → /api/auth/login Parametric segments use square brackets and become req.params: api/sites/[id].ts → /api/sites/123 → req.params.id === '123' api/users/[userId]/posts.ts → /api/users/42/posts → req.params.userId === '42' api/files/[...path].ts → /api/files/a/b/c → req.params.path === 'a/b/c' [name] — single-segment placeholder [...name] — catch-all, must be the last segment, matches one or more segments Specific routes beat dynamic ones. api/sites/new.ts wins over api/sites/[id].ts for /api/sites/new. [...rest] only matches when nothing more specific does. All HTTP methods hit the same function. Check req.method inside: export default async function(req, sw) { if (req.method === 'GET') { const users = await sw.db.query('SELECT * FROM users') return Response.json(users.data) } if (req.method === 'POST') { const body = await req.json() await sw.db.query('INSERT INTO users (email) VALUES (?)', [body.email]) return Response.json({ ok: true }) } return new Response('Method not allowed', { status: 405 }) } ## Reading route params For parametric routes, req.params holds the decoded segment values: // api/sites/[id].ts export default async function(req, sw) { const site = await sw.db.query( 'SELECT * FROM sites WHERE id = ?', [req.params.id] ) if (!site.data.length) return new Response('Not found', { status: 404 }) return Response.json(site.data[0]) } // api/files/[...path].ts export default async function(req, sw) { const file = await sw.fs.read(req.params.path) return new Response(file) } ## Secrets / env vars Set via dashboard Settings or env MCP tool. Access inside functions as sw.env.KEY_NAME: export default async function(req, sw) { const apiKey = sw.env.STRIPE_SECRET_KEY const response = await fetch('https://api.stripe.com/v1/charges', { headers: { 'Authorization': 'Bearer ' + apiKey } }) return new Response(response.body) } ## Binary files in deploy Static files go in the files parameter. Binary files (images, fonts) go in binary_files as base64: project_deploy({ project_id: "my-app", files: { "index.html": "...", "api/hello.ts": "export default async (req, sw) => Response.json({ ok: true })" }, binary_files: { "images/logo.png": "iVBORw0KGgo..." } }) ## Invalid patterns - NEVER deploy a separate backend, Express server, or external BFF — the platform IS your backend - NEVER put secrets in the files parameter (use env vars) - NEVER call api.somewhere.tech from inside a function (use sw directly) - NEVER install or import @somewhere-tech/sdk inside functions (use sw) --- ## sw.fs — File Storage (inside deployed functions) (sw.fs) Read, write, delete, and manage files in the project's storage. Direct access, no HTTP. Files are publicly servable by default unless you pass `visibility: 'private'`. ## What you get out of the box (capability list) - **File-level ACL (visibility).** Every file row carries a `visibility` flag (`public` | `private`); the platform checks it before bytes go out the door. Bucket-level ACLs alone can't give you private-by-row. - **Object versioning + content-addressed writes.** Every write generates a new immutable object with a content-hash suffix and swaps the metadata pointer atomically. A half-written file never becomes visible. - **Safe-delete pattern.** Metadata is marked deleted first; byte cleanup is deferred and reconciled by an integrity-check cron. Files never vanish before the metadata says they did. - **Versions + retention** by tier (3 / 10 / 50 / unlimited prior versions kept). - **Signed URLs with TTL** via `sw.fs.signedUrl(path, { expiresIn })`. - **SSRF protection on every platform-side fetch** (render, scrape, webhooks, AI ingest) — private and metadata IPs blocked before the request leaves. - **Full-text + path-glob search** via `fs_search`. Full reviewer-facing depth: . ## sw.fs.write(path, content, options?) const result = await sw.fs.write('/uploads/avatar.png', binaryData, { content_type: 'image/png' }) // result = { path: '/uploads/avatar.png', size: 12400, version: 1 } ## sw.fs.read(path, options?) const file = await sw.fs.read('/uploads/avatar.png') // file = Response object — file.arrayBuffer() / file.text() / file.json() // Directory listing const files = await sw.fs.read('/uploads/') // Read a line range from a text file (returns parsed JSON) const slice = await sw.fs.read('/src/index.ts', { lines: [50, 75] }) // slice = { content: '...', lines: [50, 75], total_lines: 420 } ## sw.fs.list(path, options?) // Directory listing. Pass { recursive: true } for the full subtree, // or { recursive: true, depth: 2 } to cap the walk. const all = await sw.fs.list('/src/', { recursive: true }) // all = { path: '/src/', type: 'directory', entries: [...] } ## sw.fs.glob(pattern, options?) // Match file paths against a glob. Metadata only — no content reads. // Supported: *, **, ?, {a,b,c}. const tsFiles = await sw.fs.glob('/src/api/**/*.ts') // tsFiles = { // pattern: '/src/api/**/*.ts', // matches: [{ path, size_bytes, content_type, version, updated_at }, ...] // } ## sw.fs.diff(path, options?) // Unified diff between the current file and an archived version. // Defaults to the most recent previous version. const d = await sw.fs.diff('/src/config.ts') // d = { path, from_version: 3, to_version: 4, changed_lines: 5, diff: '@@ ...' } // // Or diff against a specific version: await sw.fs.diff('/src/config.ts', { version: 2 }) ## sw.fs.delete(path) await sw.fs.delete('/uploads/old-file.txt') // Directory deletes are recursive ## sw.fs.move(from, to, opts?) await sw.fs.move('/uploads/temp/photo.jpg', '/uploads/users/alice/photo.jpg') // Instant regardless of file size. // // To replace an existing destination, pass { overwrite: true } — // this is an atomic-enough swap (destination row dropped before the // source rename, blob cleanup deferred to waitUntil). Use it instead of // the delete-then-move pattern, which can race and leave metadata // pointing at bytes that no longer exist: // // // ❌ Don't do this — racy across concurrent invocations: // await sw.fs.delete('/avatars/alice.jpg').catch(() => {}) // await sw.fs.move('/avatars/alice.upload', '/avatars/alice.jpg') // // // ✅ Do this — single atomic operation: // await sw.fs.move( // '/avatars/alice.upload', // '/avatars/alice.jpg', // { overwrite: true } // ) // // Without overwrite, a destination collision returns // VALIDATION_ERROR ("Destination ... already exists"). ## sw.fs.copy(from, to) await sw.fs.copy('/templates/welcome.html', '/users/alice/welcome.html') // Server-side copy. Instant — no bytes flow through your function. ## sw.fs.versions(path) const versions = await sw.fs.versions('/src/config.ts') // Each write creates a new version. Returns most-recent first: // [{ version: 7, size, content_type, created_at }, ...] ## sw.fs.restore(path, version) await sw.fs.restore('/src/config.ts', 5) // Restores a previous version (from sw.fs.versions) as the latest. // Original versions stay in history. ## sw.fs.stat(path) const info = await sw.fs.stat('/uploads/avatar.png') // { path, type: 'file', size, content_type, version, created_at, updated_at } ## sw.fs.search({ path?, query, limit?, max_files? }) // Full-text search across text files under a directory. A per-project // search index is maintained automatically on every fs write/delete/ // move/replace, so searches stay fast as the project grows. const hits = await sw.fs.search({ path: '/src/', query: 'TODO' }) // hits = { // query: 'TODO', path: '/src/', mode: 'fts5', total_matches: 7, // results: [ // { path: '/src/api/signup.ts', snippet: '// [TODO]: rate limit' }, // ... // ] // } // mode is 'fts5' once the index is built (typical), or 'scan' on the // first-ever search (fallback walks storage line-by-line and returns // { path, line, snippet, before, after }; the index backfills in the // background, so the next query is fast). // Defaults: path '/', limit 50, max_files 500. Binaries and files // over 1 MB are skipped by the indexer (large files truncated to // first 1 MB). Case-sensitive. ## sw.fs.replace({ path, find, replace }) // Find-and-replace on a single file, server-side — no read-modify-write // cycle. Archives the current version before writing (rollback via fs.restore). const result = await sw.fs.replace({ path: '/src/config.ts', find: "API_URL = 'https://staging.example.com'", replace: "API_URL = 'https://api.example.com'", }) // result = { ok: true, replacements: 1, path: '/src/config.ts', version: 4 } // Literal match (not regex). Text files only. ## sw.fs.uploadFromRequest(req, { path, maxBytes?, allowedTypes?, fieldName? }) // One-call upload handler for browser forms. Parses // multipart/form-data from the request, validates size + type, writes to // storage, returns the public URL the browser can render right back. // // The whole point: the developer chooses the path, the helper handles // every failure mode. Three lines instead of fifteen, and you never // JSON-stringify raw bytes (which is what corrupts images on project_patch // — use binary_files for asset deploys, sw.fs.uploadFromRequest for // runtime uploads). // // In api/upload.ts: export default async function handler(req) { const user = await sw.auth.fromRequest(req) if (!user) return new Response('unauthorized', { status: 401 }) const { url, path, size, contentType } = await sw.fs.uploadFromRequest(req, { path: `/uploads/${user.id}/${crypto.randomUUID()}.bin`, maxBytes: 5 * 1024 * 1024, // optional, 5 MB cap allowedTypes: ['image/png', 'image/jpeg'], // optional whitelist fieldName: 'file', // optional, default 'file' }) return Response.json({ url, path, size, contentType }) } // On the browser: const fd = new FormData(); fd.append('file', input.files[0]) // const r = await fetch('/api/upload', { method: 'POST', body: fd }) // const { url } = await r.json(); img.src = url // // Throws on every failure with a stable code in the message: // UPLOAD_PATH_REQUIRED, UPLOAD_NOT_MULTIPART, UPLOAD_PARSE_FAILED, // UPLOAD_FIELD_MISSING, UPLOAD_TYPE_NOT_ALLOWED, UPLOAD_FILE_EMPTY, // UPLOAD_TOO_LARGE // Wrap in try/catch and map to the HTTP status you want. // // For files larger than ~25 MB, mint a signed URL with sw.fs.uploadUrl // instead and have the browser PUT direct — keeps the bytes off your // function CPU. ## sw.fs.uploadUrl({ path, maxSize?, contentType?, expiresIn? }) // Mints a short-lived signed URL the browser can PUT bytes to directly // — without ever seeing your platform key. Use this for big uploads // (videos, large images, archives) so the bytes don't round-trip through // your function. const { url } = await sw.fs.uploadUrl({ path: '/uploads/users/alice/photo.jpg', maxSize: 10 * 1024 * 1024, // optional, defaults to tier max contentType: 'image/jpeg', // optional, '*' if omitted expiresIn: 300, // optional, seconds (default 300, max 3600) }) // Browser side: // await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'image/jpeg' }, body: file }) // Returns { path, size_bytes, content_type, version }. ## sw.fs.signedUrl(path, { expiresIn? }) // Mints a short-lived URL anyone can GET to download the file — no // platform key, no app-user JWT. Use for email attachments, image // previews, share links, "download once" handoffs. Default 1h, max 7d. const { url, expires_at } = await sw.fs.signedUrl('/uploads/invoice-42.pdf', { expiresIn: 3600, // optional, seconds (60..604800) }) // → url: 'https://api.somewhere.tech/v1/fs-signed/' // → expires_at: ISO 8601 string // Send the url in an email body, embed it in , etc. // The URL stops working at expires_at; rotating JWT_SECRET invalidates // every outstanding signed URL (bulk revocation lever). ## Public URL Files in storage are publicly accessible at: https://{subdomain}.somewhere.tech/storage/{path} const url = 'https://myapp.somewhere.tech/storage/uploads/avatar.png' // Use this URL in HTML: ## Integrity check (developer key only) // POST /v1/fs/:project_id/integrity-check { auto_clean?: bool, limit?: int, cursor?: string } // Scans file metadata for rows pointing at missing blobs — the "ghost // file" failure mode (fs.stat says exists, fs.read 404s). Returns the // list of orphans; pass auto_clean:true to delete the stale rows. // // fs.read already self-heals one row at a time on 404. This is the // proactive sweep for post-incident cleanup or mass-delete recovery. // Paginate via { cursor: } for large projects (default // 1000 rows / call, max 5000). --- ## Auth — End-User Authentication (sw.auth) Built-in user management for the people who USE your app. They sign up, log in, reset passwords, verify email — and inside deployed functions you call all of it through `sw.auth.*` without managing API keys or URLs. `sw.auth` is the same shape as `sw.email` or `sw.ai`: the platform's REST surface wrapped, scoped to your project, ready to go. ## Security model (capability list) - **Tenant-scoped signing keys (per-project HKDF derivation).** Each project's JWT signing key is derived from a master secret + project ID via HKDF-SHA256. A leaked token in project A can't forge anything in project B. - **Immediate revocation via `token_version`.** Bump the column on `platform_users` and every existing JWT for that user fails the next verify — no separate revocation store needed. - **Atomic single-use tokens** for password reset / magic link / refresh / MFA. Consumption uses `UPDATE … WHERE token = ? AND consumed_at IS NULL` so two parallel uses can't both succeed. - **MFA (TOTP)** built in: enroll / verify / unenroll / challenge + recovery codes. - **OAuth (Google)** as a first-class flow — the platform owns the callback, sets the cookie, no `passport` boilerplate. - **Header-based auto-refresh.** If the access token is in its last 10% of TTL and a refresh token rides along, the platform mints a new pair, runs the request under the refreshed identity, and returns the new pair on `X-New-Access-Token` / `X-New-Refresh-Token`. No 401-loop in your client code. - **Built-in Row-Level Security (RLS)** — covered in the `sw.db` topic. `sw.db.query(sql, params, { user })` rewrites raw queries to enforce `WHERE owner_id = ?` server-side. - **Role-based access control (RBAC) — platform layers.** Platform admin (`platform_users.is_admin`), project membership (`project_collaborators`), API-key authority (`developer | admin | cli_pair` via `api_key_authority`), and subscription tier all gate what an actor can do. Application-level RBAC for your app's OWN end-users (admin/editor/viewer) is customer-managed today via a `role` column on your users table + a check in your handler. Full reviewer-facing depth: . For agent-driven flows from your tools, the same surface is exposed as MCP (auth_signup, auth_login, auth_me, auth_refresh, auth_users_list, auth_user_delete, auth_user_update, auth_google_url) and as REST under /v1/auth/*. ## Token types Every signup/login returns three tokens: token — short-lived JWT (1 hour). Used as Bearer on protected endpoints. Sent by the browser on every API call. refresh_token — 30 days. Single-use; rotates on every refresh. Use it to mint a fresh access token without making the user log in again. session_token — opaque server-side session. Pass to logout to revoke. The smt_ developer key is for YOUR backend. App-user JWTs are for the end user's browser. `sw.auth` holds the developer key for you — you never type `smt_` inside a function. ## Sign up const { user, token, refresh_token, session_token } = await sw.auth.signup({ email, password, display_name, // optional locale, // optional, BCP-47 like "en-US" timezone, // optional, IANA like "America/Los_Angeles" }); REST: POST /v1/auth/signup · MCP: auth_signup ## Log in const { user, token, refresh_token, session_token } = await sw.auth.login({ email, password, }); REST: POST /v1/auth/login · MCP: auth_login ## Get the user from a request — start here ```typescript export default async function (req, sw) { const user = await sw.auth.fromRequest(req) if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 }) // user.id, user.email, etc. — go straight to your query const { data } = await sw.db.query('SELECT * FROM posts WHERE author_id = ?', [user.id]) return Response.json(data) } ``` `fromRequest(req)` reads the token in this order: 1. Cookie named `token`, then `auth_token`, then `session` 2. `Authorization: Bearer ` header Then validates with `sw.auth.me` under the hood (token cache included) and returns the user object — or `null` if no token, the token is expired, or the token is invalid. Never throws. This is the single call you should reach for from every protected function. ## Lower-level: validate a token you already pulled const { user } = await sw.auth.me(jwt); Use this only when you already extracted the JWT yourself (e.g. from a non-standard header, a query param, a WebSocket subprotocol). For 99 % of routes, prefer `fromRequest` — it does the extraction + validation in one call. `me` throws `AUTH_INVALID_CREDS` if the token is bad. Returns `{ user: { id, email, email_verified, display_name, locale?, timezone?, plan, plan_status } }`. `plan` is always populated (defaults to the string `'free'` for users who have never checked out). `plan_status` is `null` for free users, and `'active' | 'past_due' | 'canceled'` once they've subscribed via `sw.payments.checkoutForUser(user.id, { plan, ... })`. Gate features off these two fields directly — no separate subscription table needed. See the `payments` topic for the full flow. REST: GET /v1/auth/me with the user JWT in Authorization · MCP: auth_me ## Browser sessions — use httpOnly cookies, not tokens Browser apps should hold NO tokens at all: the happy path is cookie sessions — your backend calls `sw.auth.loginWithCookie` and the platform sets the session as httpOnly cookies the browser owns. Zero client auth code, nothing in localStorage, XSS can't steal what JS can't read, and the session refreshes server-side automatically. `platform_help({ topic: 'auth-client' })` has the exact code to paste. The full cookie-session surface (all on the runtime): // Sign in/up AND set the session cookies on the response; returns // the user. Also accept (req, { email, password }). Expected // failures (wrong password, duplicate email) surface as structured // 4xx with the real message — no try/catch needed. await sw.auth.loginWithCookie(req, email, password); await sw.auth.signupWithCookie(req, email, password); // Your Google OAuth callback route in one call: reads ?code, // exchanges it, sets the cookies, returns a 302 to redirectTo. return sw.auth.googleCallbackWithCookie(req, '/'); // Revoke the session server-side and clear the cookies. await sw.auth.logoutWithCookie(req); // Lower-level primitives the helpers wrap — for backends that // already hold a token pair (e.g. after verifyOtp or mfa.challenge) // and want THAT session as cookies. sw.auth.setSessionCookies(access, refresh); sw.auth.clearSessionCookies(); Cookie-authed requests are origin-checked server-side: a cross-origin page can't ride the cookies into your API (blocked attempts log loudly). `fromRequest` auto-refreshes expired cookie sessions and re-issues the cookies, so users stay signed in across browser restarts. ## Header-based auto-refresh (manual token mode) For clients that DO hold tokens — native apps, CLIs, non-browser runtimes, or a browser app that explicitly opted out of cookie sessions — tell the client to send both tokens on every request: Authorization: Bearer X-Refresh-Token: When the access token has expired the platform mints a fresh pair, runs the request under the refreshed identity, and returns the new pair on the response: X-New-Access-Token: X-New-Refresh-Token: The client persists the new pair and moves on — no 401-handling loop. Inside deployed functions `sw.auth.fromRequest(req)` does the same dance automatically and the platform attaches the X-New-* headers to your function's response. Per-request opt-out: `X-No-Auto-Refresh: 1`. Two flavors for guarded handlers: // Returns the user OR null — use when the route works for anonymous // visitors too (e.g. a feed that personalizes when signed in). const user = await sw.auth.fromRequest(req); // Throws 401 if not signed in — use when the route requires auth. // The handler shim converts the thrown error into a 401 response. const user = await sw.auth.requireUser(req); Enrichment (optional 2nd arg) — most apps follow up fromRequest with a SELECT against their own user-table to pull role/metadata/etc. Pass enrichFrom and the join happens for you, one DB call total: const me = await sw.auth.requireUser(req, { enrichFrom: 'members', // your table fields: ['role', 'metadata'], // optional, default * on: 'id', // optional, default 'id' (matches user.id) }) // me = { id, email, ...platform fields..., role, metadata } Platform fields always win on a name collision (id/email/etc), and a missing table or column logs server-side without throwing — your handler still gets the unenriched user back. Manual-token browser snippet (advanced — browser apps should prefer the cookie sessions above; use this only when you must hold tokens in JS): // auth.ts const ACCESS = 'sw_access_token'; const REFRESH = 'sw_refresh_token'; export function setTokens(t: { access: string; refresh: string }) { localStorage.setItem(ACCESS, t.access); localStorage.setItem(REFRESH, t.refresh); } export function clearTokens() { localStorage.removeItem(ACCESS); localStorage.removeItem(REFRESH); } export async function swFetch(input: RequestInfo | URL, init: RequestInit = {}) { const headers = new Headers(init.headers); const access = localStorage.getItem(ACCESS); const refresh = localStorage.getItem(REFRESH); if (access && !headers.has('Authorization')) headers.set('Authorization', 'Bearer ' + access); if (refresh && !headers.has('X-Refresh-Token')) headers.set('X-Refresh-Token', refresh); const res = await fetch(input, { ...init, headers }); const newA = res.headers.get('X-New-Access-Token'); const newR = res.headers.get('X-New-Refresh-Token'); if (newA && newR) { localStorage.setItem(ACCESS, newA); localStorage.setItem(REFRESH, newR); } return res; } Use it: // After signup/login: setTokens({ access: result.access_token, refresh: result.refresh_token }); // From now on, every call rotates the pair silently: const r = await swFetch('https://api.somewhere.tech/v1/db/query', { method: 'POST', body }); ## Refresh expired token (legacy / explicit) const { access_token, refresh_token, expires_in } = await sw.auth.refresh({ refresh_token, }); Old refresh tokens stop working immediately on use. Store the new pair. Prefer the header-based flow above for browser apps — explicit `/v1/auth/refresh` is for non-browser SDKs or clients that want to control rotation manually. REST: POST /v1/auth/refresh ## Password reset flow // Step 1 — request a reset email. Always succeeds (no enumeration). await sw.auth.forgot({ email }); // Step 2 — user clicks the email link, your form posts the token + new pw. await sw.auth.reset({ token, new_password }); REST: POST /v1/auth/forgot, POST /v1/auth/reset ## Email verification `sw.auth.signup` auto-sends a 6-digit code to the user's email on success — they're prompted to verify on first visit. The full surface: // Re-send a 6-digit code (15 min expiry). await sw.auth.requestEmailVerification(jwt); // User types the code into your form; you verify it. await sw.auth.verifyEmail(jwt, { code }); // Or — resend the email if it didn't arrive. await sw.auth.resendVerification(jwt); ## Magic links (passwordless sign-in) For users who don't want a password (or forgot it). `signInWithOtp` emails a one-click link; `verifyOtp` exchanges the token for a session the same shape as `login`. // Step 1 — your form posts the email; we email a 15-min single-use // sign-in link. Always succeeds (no enumeration). Auto-creates the // user if they're new. await sw.auth.signInWithOtp({ email }); // Step 2 — link points at your app with ?token=...; you exchange it. const { user, token, refresh_token, session_token } = await sw.auth.verifyOtp({ token }); REST: POST /v1/auth/magic-link, POST /v1/auth/magic-link/verify MCP: auth_send_magic_link, auth_verify_magic_link ## MFA / TOTP RFC 6238 TOTP — Google Authenticator, 1Password, Authy all work. Once enrolled, the user's normal `login` returns `{ mfa_required: true, mfa_token }` instead of a session; complete the challenge with the 6-digit code to get the session. // Enrollment — pass the user's JWT. const { secret, otpauth_uri } = await sw.auth.mfa.enroll({ token: jwt }); // Show otpauth_uri as a QR code; user scans it, types the first code. const { backup_codes } = await sw.auth.mfa.verify({ token: jwt, code }); // backup_codes is returned ONCE on first verify — show it to the // user now (null on re-verify; existing codes are preserved). // On login, if MFA is enrolled: const login = await sw.auth.login({ email, password }); if (login.mfa_required) { // Prompt the user for their TOTP code (or a backup code), then complete: const session = await sw.auth.mfa.challenge({ mfa_token: login.mfa_token, code, }); } // Remove MFA — requires a fresh TOTP or backup code, so a stolen // JWT alone can't turn it off. await sw.auth.mfa.unenroll({ token: jwt, code }); REST: POST /v1/auth/mfa/{enroll,verify,challenge,unenroll} MCP: auth_mfa_{enroll,verify,challenge,unenroll} ## Update password (logged-in user) await sw.auth.updatePassword(jwt, { current_password, new_password, }); Wipes ALL active sessions and refresh tokens on success — the user has to log in again everywhere. For OAuth-only users setting their first password, omit `current_password`. ## Update profile (logged-in user) await sw.auth.updateProfile(jwt, { display_name, // optional, set to null to clear metadata, // optional, replaces any existing metadata blob }); ## Self-service account deletion await sw.auth.deleteUser(jwt); Cascade-deletes the user's sessions, reset tokens, verification codes, and `app_users` row. Project data (your own tables) is the developer's concern — wipe it before calling. ## Admin / test-harness deletion (developer key) await auth_user_delete({ project_id, user_id }); // MCP tool // or REST: DELETE /v1/auth/users/:id?project_id=... with smt_ key For wiping throwaway test users from your own tooling, when you don't have the user's JWT. Same cascade as the self-service variant. The project must be one you own. ## Admin / test-harness edit (developer key) await auth_user_update({ project_id, user_id, display_name, metadata }); // or REST: PATCH /v1/auth/users/:id?project_id=... with smt_ key For admin tooling: edit `display_name` and/or `metadata` on any end-user in a project you own. Same field shape as the user's own PATCH /users/me. Email and password are intentionally NOT here — those have their own verification + reset flows. `metadata` is REPLACED, not merged; max 16 KB. Also accepts `banned` and `banned_reason` — see "Ban / suspend" below. ## Admin operations are NOT on the runtime `sw.auth.admin.*` is not callable inside deployed functions — any call throws `AUTH_ADMIN_REMOVED_FROM_RUNTIME`. A public handler that accepted a user id from the request body would be an account-takeover primitive, so admin actions on app users run only from your own tooling with a developer `smt_` key (MCP tools or REST), never from a request handler. The sections below are that surface. ## Ban / suspend (developer key) Block a user from signing in without deleting their data. Bans are reversible — the row stays put, sessions are wiped, future `login` returns AUTH_INVALID_CREDS (no enumeration), and any active JWT bounces with 403 AUTH_BANNED on `/me` so your SPA can log them out. await auth_user_update({ project_id, user_id, banned: true, banned_reason: 'spam' }); await auth_user_update({ project_id, user_id, banned: false }); // unban; clears reason+timestamp The `banned_reason` is staff-only — never returned to the banned user. Banning a user invalidates every active session + refresh token. REST: PATCH /v1/auth/users/:id?project_id=… with `banned` / `banned_reason` fields · MCP: auth_user_update ## Impersonation (developer key) Mint a 1-hour JWT for an end-user as YOU — useful for support tickets, test harnesses, and debugging customer reports. The token carries `impersonating: true` and `impersonator_id` claims so audit logs can distinguish impersonated traffic from genuine user traffic. const { access_token, user } = await auth_impersonate({ project_id, user_id }); // access_token is 1h, no refresh — re-mint when it expires. Banned users cannot be impersonated (returns 403 AUTH_BANNED). REST: POST /v1/auth/users/:id/impersonate · MCP: auth_impersonate ## Session management (developer key) List + revoke a user's active sessions. Tokens themselves are never returned — only session IDs, creation timestamps, and expiries. const sessions = await auth_list_sessions({ project_id, user_id }); // [{ id, created_at, expires_at }, ...] — non-expired only, max 100 await auth_revoke_session({ project_id, session_id }); // log this device out await auth_revoke_all_sessions({ project_id, user_id }); // log every device out REST: GET /v1/auth/users/:id/sessions, DELETE /v1/auth/sessions/:id, DELETE /v1/auth/users/:id/sessions MCP: auth_list_sessions, auth_revoke_session, auth_revoke_all_sessions ## Hosted auth pages Drop-in branded UI for apps that don't want to build their own forms. https://auth.somewhere.tech/login?project_id=...&redirect=https://yourapp.somewhere.tech/ Pages: `/login`, `/signup`, `/forgot-password`, `/reset`, `/verify-email`, `/magic-link`, `/mfa`. On success the page redirects to `redirect` with `#access_token=...&refresh_token=...` in the URL hash. Your SPA reads `location.hash`, stores the tokens in localStorage, and replaces history. The pages POST directly to `api.somewhere.tech` — no business logic lives on the auth subdomain. ## Lifecycle webhook (clean up your own tables on user delete) The platform's user-delete cascade only touches platform-owned tables (sessions, password resets, email verifications, the `app_users` row). Anything you wrote that references the user_id — `posts.author_id`, `comments.user_id`, files in `/avatars/`, etc. — is your concern. Without a hook, those rows orphan silently and your app starts returning rows pointing at users that no longer exist. Configure a project-scoped webhook to clean them up transactionally: // one-time setup (developer key) const { secret } = await auth_webhook_set({ project_id: 'default', url: 'https://yourapp.somewhere.tech/api/auth/webhook', }); // store `secret` server-side — we don't surface it again. // re-call with rotate_secret: true to rotate. Inside your webhook handler, verify the signature and run cleanup: // POST handler in your deployed function const sig = req.headers.get('X-Somewhere-Signature') ?? ''; const m = sig.match(/^t=(\\d+),v1=([0-9a-f]+)$/); if (!m) return new Response('bad sig', { status: 401 }); const [, t, hex] = m; if (Math.abs(Date.now() - Number(t)) > 5 * 60 * 1000) { return new Response('stale sig', { status: 401 }); } const raw = await req.text(); const expected = await sw.crypto.hmacSha256Hex(`${t}.${raw}`, secret); if (!sw.crypto.timingSafeEqual(expected, hex)) { return new Response('bad sig', { status: 401 }); } const evt = JSON.parse(raw); if (evt.type === 'auth.user.deleted') { await sw.db.query( `DELETE FROM posts WHERE author_id = ?`, [evt.user.id], ); // ...etc. for every table that references user_id } return new Response('ok'); Currently fired event: `auth.user.deleted`. Same HMAC scheme as inbox webhooks — one verifier covers both. To stop firing webhooks for the project: await auth_webhook_delete({ project_id: 'default' }); ## Logout // Cookie sessions (browser default): revoke + clear the cookies. await sw.auth.logoutWithCookie(req); // Token mode: pass the session_token and/or refresh_token. await sw.auth.logout({ session_token }); Revokes the session. The current JWT keeps working until it expires (max 1 hour) — JWTs are stateless. To force immediate logout, also rotate the user's password. ## Anonymous sessions (pre-signup) Some applications let visitors do something useful before they sign up (send a chat message, upload a file, fill a form). Track them with `sw.auth.anonSession(req)`: ```typescript export default async function (req, sw) { const session = sw.auth.anonSession(req) // session.id → 'anon_4f9b3...' (stable across requests via cookie) // session.isAnon → true // Tag rows with session.id while they're anonymous. await sw.db.query('INSERT INTO messages (user_id, body) VALUES (?, ?)', [session.id, 'hello']) // applyTo() attaches the Set-Cookie header on first visit (no-op // afterwards). One line, no manual cookie parsing. return session.applyTo(Response.json({ ok: true })) } ``` Cookie shape: `sw_anon_id=; Path=/; Max-Age=31536000; HttpOnly; Secure; SameSite=Lax`. When the visitor signs up or logs in, hand the anon id over to the real user with `migrateAnon`: ```typescript const { user, token } = await sw.auth.signup({ email, password }) const session = sw.auth.anonSession(req) const result = await sw.auth.migrateAnon({ anonId: session.id, userId: user.id, }) // result.migrated → number of rows updated // result.tables → list of tables touched ``` By default `migrateAnon` auto-detects every user-table that has a `user_id` column (skipping platform-managed `_-prefix` tables) and runs `UPDATE … SET user_id = WHERE user_id = ` on each. Override with `tables: ['messages', 'orders']` to be explicit when your schema isn't anon-id friendly. Don't reinvent this in three files of application code. ## Google OAuth **The platform owns the Google OAuth client.** You do NOT create a Google Cloud Console project. You do NOT register your own OAuth 2.0 credentials. You do NOT add authorized redirect URIs in any Google console. The platform's single OAuth client handles every project on the platform — you just call `sw.auth.googleUrl({ redirect_uri })` and the platform accepts `redirect_uri` for any verified project subdomain or claimed custom domain attached to your project. If a `redirect_uri` is rejected, the answer is either (a) the host doesn't belong to your project yet (run `domain_attach`) or (b) the project subdomain is wrong — never "go set up Google credentials". // Build the URL to send the user's browser to: const url = sw.auth.googleUrl({ redirect_uri: 'https://myapp.somewhere.tech/auth/callback' }); // ...redirect the user there. After the user approves, the platform handles the Google code exchange and then bounces them back to your `redirect_uri` with a short-lived authorization code in the query string: https://your-app.somewhere.tech/auth/callback?code=... The JWT is never put in the URL. To get the JWT, your callback function exchanges the code: const { token, user } = await sw.auth.googleExchange({ code }); Codes are single-use and expire after 60 seconds. Typical callback function — one line with the cookie helper: // api/auth-callback.ts export default async function(req, sw) { // Reads ?code, exchanges it, sets the httpOnly session cookies, // and 302s home. Tokens never touch client JS. return sw.auth.googleCallbackWithCookie(req, '/'); } Call `googleExchange` directly only when you manage tokens yourself (native apps / non-browser clients) — it returns the same `{ user, token, refresh_token, session_token }` shape as `login`. REST: GET /v1/auth/google for the URL, POST /v1/auth/google/exchange for the code. · MCP: auth_google_url ## Common pattern: protected endpoint inside a deployed function export default async function(req, sw) { const user = await sw.auth.fromRequest(req); if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 }); const posts = await sw.db.query( 'SELECT * FROM posts WHERE author_id = ?', [user.id] ); return Response.json(posts.data); } That's it. No cookie parsing, no header slicing, no try/catch on expired tokens — all of that lives inside `fromRequest`. If you find yourself writing `req.headers.get('cookie')` inside a function, you re-implemented something that already exists. ## Auth emails Password reset, email verification, and welcome emails are platform email — they're sent from a platform-owned address (noreply@somewhere.tech) on every project's behalf. No setup required: every project gets working auth out of the box. Customize the subject + HTML in dashboard Settings → Email Templates (or via the email-templates API). The from-address is platform-owned and not configurable per project — that's by design, so deliverability isn't gated on each user verifying their own domain. The per-project verified sender domain only matters for sw.email.send — your app's own outbound mail (marketing, receipts, transactional). Auth emails bypass that check. --- ## Projects — Full Lifecycle (projects) Every app on the platform is a project. A project owns: - a subdomain (https://{subdomain}.somewhere.tech) - a database - a file store - environment variables - deployed functions and static files ## project_create({ name, subdomain?, description? }) Creates an empty project. If subdomain is omitted, one is generated from the name. Subdomains must be globally unique, 3–32 chars, [a-z0-9-]. Returns { project_id }. ## project_list() Returns every project on your account, newest first. Each row: { id, name, subdomain, description, created_at, last_deployed_at } ## project_get({ project_id }) Single project record + tier + deploy state. ## project_view_urls({ project_id }) Returns the project's URLs and current version. The bare subdomain serves your latest deploy. `?env=dev` is preserved as an internal opt-in for testing the same bytes from the dev slot. ## project_deploy({ project_id, files, functions?, binary_files? }) Full replacement, served live. See the deploy topic for the exact semantics and when to choose project_patch instead. ## project_patch({ project_id, path, content? | find?+replace?, delete_files?, expected_version? }) Single-file edit, served live. Two modes — `content` rewrites the whole file; `find`+`replace` does a server-side substring substitute (~200 bytes on the wire, preferred for tweaks). Unchanged files are preserved. `delete_files` removes paths. See the deploy topic for the full shape. ## project_rollback({ project_id }) Revert the last deploy. Customers immediately see the previous working version. Instant — no rebuild. ## project_deploys({ project_id }) List the last 20 deploy versions: [{ version, message, created_at, live }] ## project_restore_version({ project_id, version }) Restore a specific past version as the live version. ## project_undeploy({ project_id }) Take the project off the public subdomain. Files and database stay. Reverse with project_deploy. ## project_archive({ project_id }) / project_unarchive({ project_id }) Hide from the project list and revoke the subdomain. Data preserved. Use for old projects you don't want cluttering project_list but might revive later. ## project_rename({ project_id, name?, description? }) Renames the display name and/or description. Subdomain unchanged. ## project_rename_subdomain({ project_id, subdomain }) Changes the live URL. Old subdomain stops resolving immediately. Any links the user has shared elsewhere will 404 — warn before calling. ## project_export({ project_id }) Pull every file in a project (static + functions) in one call — works from any MCP client. Pair with project_deploy to write back. (CLI: somewhere pull does the same to disk.) ## project_transfer({ project_id, to_email }) Transfer ownership. If the target email isn't a platform account yet, they get an invite email. Includes the project's data, files, functions, and env vars. ## project_delete({ project_id }) → project_delete_confirm({ project_id, code }) Two-step. First call returns a 6-digit code with error "CONFIRMATION_REQUIRED". Pass that code to project_delete_confirm within 10 minutes. Second call wipes everything — database, storage, functions, env vars, custom domains. Irreversible. ## deploy_status({ project_id }) Returns deploy metadata: current version, last-deploy timestamp, file counts per slot. Mostly useful as a sanity check that a deploy landed. ## Invalid patterns - Don't call project_deploy when you only want to change one file — use project_patch (deploy is full replacement and silently deletes any function not in the payload). - Don't rename the subdomain on a live project without warning the user — old links break. - Don't call project_delete in a script without surfacing the confirmation code to a human first. --- ## Search — Two Surfaces (search) The platform has two search systems for two different jobs. ## File search — sw.fs.search (inside functions only) Full-text search across the project's text files. Used by code agents to grep code, search docs, find TODOs. const hits = await sw.fs.search({ path: '/src/', query: 'TODO', limit: 50 }) This is documented in the sw.fs topic. Per-project full-text index, kept in sync automatically on every fs write/delete/move/replace. ## Semantic search — search_index_* MCP tools Vector search for product features (support search, semantic recommendations, RAG retrieval). Embeddings are generated with @cf/baai/bge-base-en-v1.5 (768-dim, cosine distance). Each project can hold many named indexes. Indexes can be managed from MCP (search_index_create / upsert / query etc.) and from inside your deployed function via sw.search.* — same operations, no API key juggling. See "From inside a function" below. ## search_index_create({ project_id, name }) Idempotent — returns the existing index if name already exists. Returns { name, project_id, dimensions, distance_metric, created_at }. ## search_index_list({ project_id }) Lists every index on the project: [{ name, item_count, created_at }]. ## search_delete({ project_id, index, ids? }) Delete from a search index. Pass ids (a JSON array of item id strings) to remove specific items; omit ids to drop the entire index AND all items in it (irreversible). ## search_upsert({ project_id, index, items }) Insert or replace items. items is a JSON array of: { id, content, metadata? } search_upsert({ project_id: "my-app", index: "help", items: JSON.stringify([ { id: "help-1", content: "How do I reset my password?", metadata: { category: "account" } }, { id: "help-2", content: "Where can I see my billing history?", metadata: { category: "billing" } } ]) }) content is embedded server-side — you never compute embeddings yourself. metadata is returned alongside results but NOT searched. Max 100 items per call. ## search_query({ project_id, index, query, limit? }) Returns top N matches by cosine similarity: { results: [{ id, score, content, metadata }] } search_query({ project_id: "my-app", index: "help", query: "I forgot my login", limit: 5 }) Score is 0–1. Higher is closer. Limit defaults to 10, max 100. ## From inside a function — sw.search.* Same surface, same args, no API key. Use sw.search inside a deployed function — the platform binding handles auth and project scope. // Index management await sw.search.createIndex('help') const idxs = await sw.search.listIndexes() await sw.search.deleteIndex('help') // Upsert await sw.search.upsert({ index: 'help', items: [ { id: 'help-1', content: 'How do I reset my password?', metadata: { category: 'account' } }, ], }) // Query const { results } = await sw.search.query({ index: 'help', query: 'I forgot my login', limit: 5, }) // Remove specific items await sw.search.remove({ index: 'help', ids: ['help-1'] }) ## When to use which - Searching files (code, markdown, configs in your project) → sw.fs.search - Semantic / "find similar meaning" over your app's content → sw.search.* (or search_index_* MCP) - Searching log bodies or analytics → use sw.logs / sw.analytics.query ## Invalid patterns - Don't put PII in metadata if you don't have to — it's stored with the embeddings. - Don't reindex a whole catalog on every write; batch upserts of 100. - Don't expect exact-match — semantic search ranks by meaning, not keyword overlap. --- ## Render — Screenshots and PDFs (render) Render web pages or HTML strings to images or PDFs. Useful for generating social cards, invoices, report exports, OG images. Both endpoints either return the bytes inline (base64) or write straight to your project's file store and return the storage path. ## render_screenshot({ url? | html?, width?, height?, format?, quality?, full_page?, wait_for?, project_id?, storage? }) render_screenshot({ url: "https://example.com", width: 1200, height: 630, format: "png", full_page: false }) // → { format: "png", bytes: "iVBORw0KGgo...", size_bytes: 87421 } Defaults: 1280x800, PNG. Set full_page: true to capture the entire scrollable page. format: "png" | "jpeg" | "webp". quality (1–100) applies to jpeg/webp. wait_for is a CSS selector to wait for before capture, useful when the page lazy-loads content: wait_for: "#chart-loaded" Direct-to-storage: render_screenshot({ url: "https://...", project_id: "my-app", storage: "/renders/og-2026-04.png" }) // → { storage_path: "/renders/og-2026-04.png", size_bytes: 87421 } // File is now servable at https://my-app.somewhere.tech/storage/renders/og-2026-04.png ## render_pdf({ url? | html?, format?, landscape?, print_background?, wait_for?, project_id?, storage? }) render_pdf({ html: "

Invoice #123

...", format: "Letter", print_background: true }) // → { format: "pdf", bytes: "JVBERi0...", size_bytes: 14821 } Defaults: Letter, portrait, backgrounds rendered. format: "A4" | "A3" | "Letter" | "Legal" | "Tabloid". landscape: true rotates the page. Same storage option as screenshot — pass project_id + storage to write straight to the file store. ## Common patterns Generate an OG image and serve it: // 1. Render once when content publishes await render_screenshot({ html: `
Post title
`, project_id: "my-app", storage: `/og/${post.slug}.png`, width: 1200, height: 630 }) // 2. Reference in your meta tags Generate a PDF invoice on demand (inside a deployed function): export default async function(req, sw) { const { invoiceId } = await req.json() const html = await renderInvoiceHTML(invoiceId, sw) // sw.render.pdf returns a raw Response when no storage path is set — // pipe it straight to the client. return sw.render.pdf({ html, format: 'Letter' }) } Or write the PDF to file storage and return its URL: export default async function(req, sw) { const { invoiceId } = await req.json() const html = await renderInvoiceHTML(invoiceId, sw) const result = await sw.render.pdf({ html, format: 'Letter', storage: `/invoices/${invoiceId}.pdf`, }) // result = { storage_path: '/invoices/...', size_bytes, content_type } return Response.json({ url: `https://${sw.subdomain}.somewhere.tech/storage${result.storage_path}` }) } ## Limits - 30 second hard timeout per render. Pages that take longer fail. - Inline returns hit JSON size limits — use storage for >5MB outputs. - One render at a time per project (no concurrent burst). ## Invalid patterns - Don't pass user-controlled URLs without a domain allowlist — the renderer will fetch any URL you give it. - Don't render on every page load — cache to /storage and reuse. --- ## Video — Upload, Stream, Manage (video) Upload video files, get HLS + DASH playback URLs, list and delete. The platform stores videos on a streaming service and gives you direct upload URLs the client uses without ever touching your worker. ## video_upload_url({ project_id, title?, max_duration_seconds?, require_signed_urls? }) Get a one-time upload URL. The client POSTs the video bytes directly to this URL using the TUS resumable upload protocol — your function never sees the bytes. video_upload_url({ project_id: "my-app", title: "Product walkthrough", max_duration_seconds: 600 }) // → { upload_url: "https://upload.videodelivery.net/tus/...", video_id: "vid_abc" } max_duration_seconds: 30–21600 (6 hours), defaults to 3600 (1 hour). Uploads exceeding the limit are rejected at ingest. require_signed_urls: true means playback requires a short-lived signed token. Use for paid content. Default false (public playback). ## video_list({ project_id, limit? }) Returns videos on the project, newest first. limit defaults to 50, max 200. [ { id: "vid_abc", status: "ready" | "queued" | "inprogress" | "error", duration_seconds: 184, size_bytes: 24310291, thumbnail: "https://...thumbnails/thumb.jpg", preview: "https://...preview.mp4", hls_url: "https://...manifest/video.m3u8", dash_url: "https://...manifest/video.mpd", created_at: "2026-04-25T...", ready: true } ] ## video_get({ id }) Same shape as a list item. Useful for polling status after upload — ready: false → ready: true once encoding finishes (typically within minutes). ## video_delete({ id }) Permanently delete the video and all derived files. Irreversible. ## Common patterns Browser upload flow: // 1. Backend mints the upload URL const { upload_url, video_id } = await video_upload_url({ project_id }) // 2. Frontend uploads via TUS (use the tus-js-client library) const upload = new tus.Upload(file, { endpoint: upload_url, onSuccess: () => savedVideoId(video_id) }) upload.start() // 3. Backend polls video_get until ready, then shows the player const v = await video_get({ id: video_id }) if (v.ready) renderPlayer(v.hls_url) Embed in a page: // Use hls.js for cross-browser HLS playback. ## Invalid patterns - Don't proxy video bytes through your worker — the upload_url accepts the bytes directly from the client. That's the whole point. - Don't poll video_get every 100ms; once a second is plenty. - Don't issue the same upload_url to multiple clients — each URL is single-use. --- ## Analytics — Track Events, Query Aggregates (analytics) Per-project event tracking. Write events from anywhere (function, browser, MCP), then aggregate them by hour / day / event / user. Backed by an append-only event store. No per-user PII sanitization — treat what you write as eventually visible to anyone with project access. ## analytics_track({ project_id, event, user_id?, properties?, page?, referrer?, user_agent? }) analytics_track({ project_id: "my-app", event: "signup", user_id: "u_alice", properties: JSON.stringify({ plan: "builder", referrer: "blog" }) }) event: required, max 128 chars. Pick a stable name — you'll filter on this. Convention: snake_case verbs (signup, post_published, checkout_started). properties: JSON object, max 8KB total. Up to 20 numeric values are auto-aggregatable in queries (sum / avg). String values are kept as labels for grouping but not summed. page, referrer, user_agent are convenience fields for browser-side tracking; treat them as optional metadata. Returns { ok: true }. Track is fire-and-forget — there is no per-event read API. Aggregate via analytics_query. ## analytics_query({ project_id, event?, from?, to?, group_by?, limit? }) analytics_query({ project_id: "my-app", event: "signup", from: "2026-04-01", to: "2026-04-25", group_by: "day" }) // → [ // { day: "2026-04-01", count: 12, signup_count: 12 }, // { day: "2026-04-02", count: 18, signup_count: 18 }, // ... // ] event: omit to query across all events. from, to: ISO-8601 string ("2026-04-01") or unix-ms integer. group_by: "hour" | "day" | "event" | "user". Omit to get raw rows (most recent first). limit: defaults to 1000, max 10000. When group_by includes a numeric property in the events, the query returns sum_ and avg_ columns alongside count. ## Common patterns Funnel: // Track stages analytics_track({ project_id, event: "checkout_started", user_id }) analytics_track({ project_id, event: "checkout_completed", user_id }) // Daily conversion const started = await analytics_query({ event: "checkout_started", group_by: "day" }) const completed = await analytics_query({ event: "checkout_completed", group_by: "day" }) Per-user activity: analytics_query({ project_id, group_by: "user", limit: 100 }) // → top 100 users by event volume Browser-side tracking from your app (inside a deployed function). Derive user identity from the signed app-user JWT — never trust an ID sent by the browser. export default async function(req, sw) { const user = await sw.auth.fromRequest(req) // null if signed out const body = await req.json() await sw.analytics.track(body.event, { user_id: user?.id, // server-derived, signed properties: body.properties, page: req.headers.get('referer'), user_agent: req.headers.get('user-agent'), }) return Response.json({ ok: true }) } Query inside a function the same way: const today = await sw.analytics.query({ event: 'checkout_completed', group_by: 'day' }) ## Invalid patterns - Don't write log lines through analytics — use sw.logs. Analytics is for things you want to count and aggregate, not narrative logs. - Don't put PII in the event NAME (it goes into every grouping). Put it in properties if you must. - Don't expect millisecond-level read-after-write — events are queryable within seconds, not instantly. --- ## Inbox — Inbound Email (inbox) Inbound email is domain-bound. Create addresses on a custom domain that has been added to the project and has inbound email routing enabled (enable it with `domain_enable_email`). This prevents shared-platform-address confusion and stops users from claiming mailboxes on domains they don't own. ## inbox_create_address({ project_id, address, label?, kind?, webhook_url? }) Create an inbox address, for example support@example.com. address must be on a verified project custom domain with inbound email enabled. kind defaults to "admin" — the address is shown in the project's Email tab in the dashboard. Pass kind:"app" when minting per-user mailboxes at runtime from your deployed app, so they don't flood the admin view. sw.inbox.createAddress() from a deployed function defaults to "app" for the same reason. If webhook_url is provided, it must be https://. The create response returns webhook_secret once; store it and verify X-Somewhere-Signature on incoming webhooks. ## inbox_addresses({ project_id, kind? }) List inbox addresses on the project. Defaults to kind="admin" (dashboard-managed). Pass kind="app" to enumerate runtime-minted ones, kind="all" to see both. webhook_secret is never echoed back after creation. ## inbox_delete_address({ id }) Remove the address. New mail to it bounces. Existing messages stay queryable. ## inbox_list({ project_id, address_id?, unread?, q?, limit? }) Most recent messages first. limit defaults to 50, max 200. Pass address_id to scope to one inbox, unread:true to skip messages already marked read, or q to free-text search across subject, body preview, and sender (tokens match as prefixes — "ord" matches "order"). [ { id: "msg_abc", mail_from: "alice@example.com", mail_to: "support@example.com", subject: "Re: order #123", text_preview: "Just confirming this shipped...", has_html: true, attachment_count: 2, read_at: null, received_at: "2026-04-25T...", size_bytes: 4218 } ] ## inbox_get({ id, include_html? }) Single message + raw MIME URL. Pass include_html:true to include the stored html_preview field (capped at ~50KB). Attachment metadata is always returned when present — each entry includes filename, content_type, and size_bytes. Use inbox_attachment to download. ## inbox_mark_read({ id, read? }) Toggle the read flag. read defaults to true. Idempotent. Use to drive "unread badge" UIs and to gate webhook-fanout retries. ## inbox_attachment({ id, index }) Stream one attachment by zero-based index from inbox_get.attachments. Returns binary bytes with original content-type and filename. ## inbox_delete({ id }) Delete the message, raw MIME object, and stored attachment objects. Irreversible. ## inbox_reply({ id, body? | text? | html?, subject? }) Send a threaded reply to an inbound message. Sends from the same address that received the message and sets In-Reply-To / References so Gmail / Outlook / Apple Mail render it inline with the original thread. Subject is auto-prefixed with "Re:" if it isn't already. Requires the receiving domain to be verified as a sender domain on the project (otherwise replies would be unsigned and bounce). Same sender-domain verification as /v1/email/send. await sw.inbox.reply(messageId, { body: 'Thanks, refunding now.' }) The primary use case: an agent that watches the inbox, drafts replies via sw.ai.complete, and ships them with one call. ## inbox_send({ address_id, to, subject, body? | text? | html? }) Start a NEW conversation from one of your inbox addresses (e.g. hello@yourdomain.com). Same sender-domain check as inbox_reply. The platform issues a Message-ID with the outbound so the recipient's reply lands back in the same thread automatically — viewable via inbox_threads / sw.inbox.threads(). await sw.inbox.send(addressId, { to: 'newcustomer@example.com', subject: 'Welcome to our beta', body: 'Thanks for signing up — here\\'s your login.' }) Use inbox_reply for replies and inbox_send for new threads. Both log to the same conversation view. ## inbox_threads({ project_id, address_id?, include_spam?, limit? }) List conversation threads — one entry per unique thread_root, with counts and last-message metadata aggregated across inbound + outbound rows. Default-hides threads whose latest inbound is spam_suspect. [ { thread_root: "", last_at: "2026-05-11T14:22:18Z", first_at: "2026-05-09T09:11:02Z", message_count: 4, unread_count: 1, last_subject: "Re: refund #1421", last_counterparty: "alice@example.com", last_direction: "in" } ] ## inbox_thread_get({ project_id, root }) Full conversation by thread_root — all inbound + outbound messages sorted by time. Use this to render the conversation view in an admin UI or to feed prior context into sw.ai.complete before drafting a reply. const { messages } = await sw.inbox.thread(threadRoot) for (const m of messages) { if (m.direction === 'in') console.log('←', m.mail_from, m.text_preview) else console.log('→', m.to, m.subject) } ## Spam handling Every inbound message is checked against the Authentication-Results header (SPF/DKIM/DMARC verdicts from the receiving relay) and against your project's allow/deny rules. The result is stored on the message as spf_result / dkim_result / dmarc_result / spam_suspect. Default flag logic: - Allow rule match → never spam_suspect (allow wins) - Deny rule match → spam_suspect = true - DMARC fail → spam_suspect = true - SPF fail AND DKIM fail → spam_suspect = true - Otherwise → spam_suspect = false inbox_list and inbox_threads hide spam_suspect by default. Pass include_spam:true to include them (e.g. a "Show spam" view in an admin UI). The flag is also visible on inbox_get so you can render a "Failed authentication" badge. ## inbox_rule_list({ project_id, address_id? }) List allow/deny rules. Returns project-wide rules + rules scoped to the address_id (when provided). ## inbox_rule_create({ project_id, address_id?, pattern, action }) Create an allow or deny rule. pattern: - "alice@example.com" → exact mailbox - "example.com" → domain (and subdomains) action: "allow" or "deny". Omit address_id to apply project-wide. // Whitelist a sender whose DMARC keeps failing await sw.inbox.rules.create({ pattern: 'newsletter@partner.com', action: 'allow' }) // Blacklist a domain await sw.inbox.rules.create({ pattern: 'evil.example', action: 'deny' }) ## inbox_rule_delete({ id }) Remove a rule. Future inbound mail re-evaluates without it. ## From inside a deployed function sw.inbox mirrors the REST surface — all calls scoped to this project: await sw.inbox.list({ unread: true, limit: 25 }) await sw.inbox.list({ q: 'refund' }) // search subject/body/sender await sw.inbox.list({ include_spam: true }) // include flagged messages await sw.inbox.get(id, { include_html: true }) await sw.inbox.reply(id, { body: 'Thanks.' }) // threaded reply await sw.inbox.send(addressId, { // new thread to: 'alice@x.com', subject: 'Hi', body: '...' }) await sw.inbox.threads({ limit: 25 }) // grouped conversations await sw.inbox.thread(rootId) // single conversation await sw.inbox.markRead(id) // or sw.inbox.markRead(id, false) const res = await sw.inbox.attachment(id, 0) // Response — stream/download const raw = await sw.inbox.raw(id) // Response — RFC-822 source await sw.inbox.delete(id) // remove message + attachments await sw.inbox.createAddress({ address, webhook_url }) await sw.inbox.deleteAddress(addressId) await sw.inbox.rules.list() await sw.inbox.rules.create({ pattern: 'evil.com', action: 'deny' }) await sw.inbox.rules.delete(ruleId) ## Common pattern: webhook-first support inbox // 1. Create once during setup await inbox_create_address({ project_id, address: 'support@yourdomain.com', label: 'Support', webhook_url: 'https://yourdomain.com/api/inbox-webhook' }) // 2. In your webhook handler, verify X-Somewhere-Signature using the // webhook_secret returned at creation, then persist/route the event. // The platform retries failed/stale webhook deliveries from cron. // 3. Poll as a fallback/admin view const { messages } = await inbox_list({ project_id, unread: true }) for (const m of messages) { await processSupportTicket(m) await inbox_mark_read({ id: m.id }) } ## Limits | Tier | Addresses | Stored msgs | Retention | |---------|-----------|-------------|-----------| | Free | 1/project | 100/project | 30 days | | Builder | 10/project| 10,000 | 90 days | Past the message cap, FIFO eviction happens at insert time. A daily retention sweep deletes anything older than the window. Both the cap and retention apply per-project, not per-address. ## Invalid patterns - Don't create inbox addresses on platform domains like somewhere.tech or somewhere.site — use a custom domain owned by the project. - Don't skip webhook signature verification for sensitive workflows. - Don't delete messages until you've fully processed them; deletion is irreversible. - Don't expect retention beyond the tier window — schedule processing promptly or copy the raw .eml to your own storage. --- ## Calls — Real-Time Audio/Video Sessions (calls) Build video calling, screen sharing, or live audio rooms. The platform provides session creation; the heavy lifting (SFU mixing, NAT traversal, SDP negotiation) runs on the platform's media backend. Your code only deals with session IDs and SDP payloads — opaque blobs you pass through. ## calls_new_session({ project_id, thirdparty? }) calls_new_session({ project_id: "my-app" }) // → { session_id: "sess_xyz", project_id: "my-app" } session_id: pass to subsequent track operations from the browser. thirdparty: true → the media backend treats the session as pure data (no platform-originated tracks). Default false. Most apps don't need this. Sessions are ephemeral — the platform doesn't persist session_id. Once both peers leave, the session is gone. ## Track operations (REST only — call directly from the browser) After session creation the browser drives the call lifecycle by calling these endpoints with its peer connection's SDP: POST /v1/calls/sessions/:session_id/tracks Body: { sessionDescription: { type, sdp }, tracks: [...] } Add local tracks to the SFU. Returns SDP answer. PUT /v1/calls/sessions/:session_id/renegotiate Body: { sessionDescription: { type, sdp } } Renegotiate when adding/removing tracks mid-call. PUT /v1/calls/sessions/:session_id/tracks/close Body: { tracks: [...], sessionDescription: { type, sdp } } Close specific tracks (mute camera, stop screen share). These are called from the browser with an app-user JWT (the same JWT you use for db_query / fs_read from a logged-in user). The smt_ developer key is never required for the browser path — keep it server-side. SDP payloads are passed straight through to the media backend. ## Common pattern: 1:1 video call // 1. Backend mints a session for each call const session = await calls_new_session({ project_id }) // Send session.session_id to BOTH peers via realtime / WebSocket // 2. Each browser: const pc = new RTCPeerConnection() pc.addTrack(localVideo) const offer = await pc.createOffer() await pc.setLocalDescription(offer) // 3. Push the offer to the SFU const r = await fetch(`/v1/calls/sessions/${sessionId}/tracks`, { method: 'POST', headers: { Authorization: 'Bearer ' + jwt }, body: JSON.stringify({ sessionDescription: { type: 'offer', sdp: offer.sdp }, tracks: [{ location: 'local', mid: '0', trackName: 'video' }] }) }) const { sessionDescription } = await r.json() await pc.setRemoteDescription(sessionDescription) The wire protocol mirrors a standard WebRTC + SFU exchange — any client that speaks the offer/answer SDP dance works. ## When to use calls vs realtime - realtime — text chat, presence, score updates, anything message-shaped - calls — actual audio/video bytes (1:1 calls, group rooms, broadcasts) ## Invalid patterns - Don't try to inspect or modify SDP payloads on the server — they're passed through verbatim and are very specific to the peer's hardware. - Don't store session_ids long-term — they're not durable, the SFU drops them as peers leave. - Don't run the call's signaling through your function on every track add — the browser talks to /v1/calls/sessions/* directly, your worker just hands out fresh session_ids. --- ## sw.ai — AI Models (inside deployed functions) (sw.ai) ## Default model recommendation **Production default: `claude-sonnet-4-6`.** Use a production-capable paid model for user-visible responses. **For development / testing / scripts: a free model** (`@cf/meta/llama-4-scout-17b-16e-instruct`, on `provider: 'workers-ai'`). No activation needed, included on every plan, fine for smoke tests where quality doesn't matter. When you're showing the app to a customer, use Sonnet. When you're iterating locally or testing a flow, use the free model. ## sw.ai.chat(options) // sw.ai.complete(options) is an alias — same args, same return. // Default pick — paid, activation required (one click, one time). const result = await sw.ai.chat({ provider: 'anthropic', model: 'claude-sonnet-4-6', messages: [{ role: 'user', content: 'Write a haiku' }] }) // result.content is a content-block array (text + tool_use // blocks). result.text is a flattened text convenience. // Free model — for development, smoke tests, scripts. Quality is // lower-capability development model; use only where its output quality is acceptable // more than response quality. const dev = await sw.ai.chat({ provider: 'workers-ai', model: '@cf/meta/llama-4-scout-17b-16e-instruct', messages: [{ role: 'user', content: 'Summarize this: ...' }], max_tokens: 1024 }) // dev = { content: '...', tokens: { input: 45, output: 200 }, cost: { total: '...' } } // (Avoid reasoning models at low max_tokens — they burn 2k+ tokens on // chain-of-thought before producing output. See "Free models" section // below; ai_catalog marks which models reason.) // Paid Sonnet again, for clarity: const sonnet = await sw.ai.chat({ provider: 'anthropic', model: 'claude-sonnet-4-6', messages: [{ role: 'user', content: 'Write a haiku' }] }) // result.content is a content-block array (text + tool_use // blocks). result.text is a flattened text convenience. // result.stop_reason is 'end_turn' | 'tool_use' | 'max_tokens' | ... // Paid model — same activation gate, billed per token at the rate in /v1/pricing. // Models: grok-4 (top quality), grok-4-fast (cheap+fast), // grok-3-mini, grok-code-fast-1. const grokResult = await sw.ai.chat({ provider: 'xai', model: 'grok-4-fast', messages: [{ role: 'user', content: 'Explain quicksort in two sentences.' }], max_tokens: 256 }) // grokResult.text is the reply; grokResult.content is reshaped into the // the same content-block array shape so client code is uniform. // Paid model — same activation gate, billed per token at the rate in /v1/pricing. // Models: gpt-5.5 (frontier), gpt-5.5-pro (highest), gpt-5.4 (lower-cost // frontier), gpt-5.4-mini (cheap default), gpt-5.4-nano // (cheapest), gpt-5.4-pro. const gptResult = await sw.ai.chat({ provider: 'openai', model: 'gpt-5.4-mini', messages: [{ role: 'user', content: 'Explain quicksort in two sentences.' }], max_tokens: 256 }) // gptResult.text is the reply; gptResult.content is reshaped into the // the same content-block array shape so client code is uniform. // conversation_id works on every provider — pass it and the platform // replays prior turns from the per-project database. stream is the only // field still gated to anthropic. ## Flex tier (discounted, async) `service_tier: 'flex'` is available for supported `provider: 'openai'` calls. The request is served from spare-capacity infrastructure when available. Operational properties: - Cost is billed at the flex rate shown in current pricing data and `ai_usage` rows. - Latency can be higher than standard service tier. - Capacity can be unavailable and return a 429-style upstream error. Use flex for cron jobs, queue workers, batch enrichment, summarization, and reports where delayed completion or retry is acceptable. Do not use flex for latency-sensitive request paths unless the product behavior tolerates fallback. Opt in by passing `service_tier: 'flex'`. Default is `standard`. The field is valid only for provider/model combinations documented as supporting flex. ```js const flexResult = await sw.ai.chat({ provider: 'openai', model: 'gpt-5.4-mini', service_tier: 'flex', messages: [{ role: 'user', content: 'Summarize this article…' }], max_tokens: 512 }) ``` On capacity errors, retry later or fall back to standard service tier if the application path requires immediate completion. ```js try { return await sw.ai.chat({ provider:'openai', model:'gpt-5.4-mini', service_tier:'flex', messages }) } catch (err) { return await sw.ai.chat({ provider:'openai', model:'gpt-5.4-mini', messages }) } ``` Use `/v1/pricing` and `ai_usage` for current model rates and realized costs. ## Conversation history (all providers) Pass conversation_id and the platform stores+replays the chat for you. Prior turns are loaded from this project's database, prepended to your messages, and sent to the model. After the response, your new user message(s) and the assistant reply are saved under the same id. Next call: send only the new user turn. Works on every provider — anthropic, openai, xai, workers-ai. You can even mix providers under the same conversation_id; whichever model you call next sees the full history. (Summarization, when compaction: 'summarize' triggers it, always runs on a fast model — billed normally.) The only field still pinned to anthropic is stream. // First call — pick any id (uuid or your own scheme), up to 128 chars await sw.ai.chat({ provider: 'anthropic', model: 'claude-sonnet-4-6', conversation_id: 'c_abc123', messages: [{ role: 'user', content: "What's the capital of France?" }] }) // → { content: [...], text: 'Paris.', conversation_id: 'c_abc123', ... } // Second call — server loads the prior turn, you only send the new one await sw.ai.chat({ provider: 'anthropic', model: 'claude-sonnet-4-6', conversation_id: 'c_abc123', messages: [{ role: 'user', content: 'And of Spain?' }] }) If the combined input would exceed the model's 200K context window, the oldest history messages are dropped automatically — the response carries conversation_truncated: true. If the conversation_id doesn't exist, it's created on first save. ## History caps (per call) Trim what's loaded with two optional fields. Oldest user/assistant messages drop first; the new turn in messages and your system prompt are never dropped. The smaller cap wins. await sw.ai.chat({ provider: 'anthropic', model: 'claude-sonnet-4-6', conversation_id: 'c_abc', history_max_messages: 20, // default 50 history_max_tokens: 4000, // default 8000 (char/4 estimate) messages: [{ role: 'user', content: 'next question' }] }) Use these to keep a long-running chat cheap and predictable. The model's 200K window still applies on top. ## Compaction (preserve context across overflow) By default, messages that overflow the caps above are dropped — the context is gone. Pass compaction: 'summarize' to fold them into a rolling summary instead. Haiku produces the summary; the platform prepends it to your system prompt on every subsequent call so the model still "knows" what was discussed. await sw.ai.chat({ provider: 'anthropic', model: 'claude-sonnet-4-6', conversation_id: 'c_abc', history_max_messages: 10, compaction: 'summarize', // default: 'truncate' messages: [{ role: 'user', content: 'continue' }] }) The response carries conversation_summarized: true on calls where summarization actually ran. Cost: one extra Haiku request per overflow event (~1¢, billed normally). The summarizer falls back to truncate if the upstream call fails — your call still returns a result. Inspect the rolling summary via ai_conversation_list (pass a conversation_id); it shows in the conversation row's summary field. Manage conversations from outside functions via the MCP tools ai_conversation_list (pass a conversation_id to fetch one) / ai_conversation_delete, or the matching REST endpoints (GET /v1/ai/conversations, GET /v1/ai/conversations/:id, DELETE /v1/ai/conversations/:id — all take ?project_id=). Pass include_summarized=1 on the get endpoint to also see messages already folded into the summary. Not supported with stream: true or with provider: 'workers-ai' / 'xai' / 'openai' yet. ## System prompt — pass either way You can supply the system prompt as a top-level field OR as an inline `role: 'system'` message; the platform normalizes both shapes before dispatch, so the same code works on every provider. // Top-level await sw.ai.chat({ provider: 'anthropic', system: 'Be terse.', messages: [...] }) // Inline (workers-ai / openai dialect) await sw.ai.chat({ provider: 'anthropic', messages: [ { role: 'system', content: 'Be terse.' }, { role: 'user', content: 'Hi' } ] }) Both produce identical behavior — no need to branch on provider. ## Multi-chat — letting end users keep a list of conversations For chat apps that want a Claude.ai-style sidebar of prior threads, list / fetch / delete via `sw.ai.conversations.*`: // Inside a deployed function, scoped to the signed-in user: const user = await sw.auth.requireUser(req); const scoped = sw.ai.scoped(user.id); // also: sw.ai.forUser(user.id) // Sidebar payload — most recent first. const { conversations } = await scoped.conversations.list({ limit: 50 }); // conversations[i] = { id, client_conversation_id, subject_type, // subject_id, created_at, updated_at, // summary_updated_at, message_count, last_message_preview } // Full transcript for one selected thread. const full = await scoped.conversations.get('weather-chat-7'); // full.messages = [{ role, content, created_at }, ...] // \"Delete this chat\" button. await scoped.conversations.delete('weather-chat-7'); // \"New chat\" is just a fresh conversation_id passed to .chat(): await scoped.chat({ provider: 'anthropic', messages: [{ role: 'user', content: text }], conversation_id: 'weather-chat-' + Date.now(), }); // \"Regenerate from this point\" — fork the conversation under a // new id (optionally truncated to message N) so the original stays // intact and the dev can let the user try a different prompt. await scoped.conversations.fork('weather-chat-7', 'weather-chat-7-alt', { upToMessageId: 42, // optional; omit to copy full history }); Pattern: store the active `conversation_id` in app state (URL query param, useState, etc), pass it on every `.chat()` call, render the list from `.conversations.list()`. Apps that only need a single chat (RailTime-style) skip the list and pick one stable id. ## Error codes Every `sw.ai.*` failure rejects with an `Error` whose `.code` and `.status` fields are stable — branch on those instead of regex-matching `.message`. The contract: CONV_HISTORY_UNAVAILABLE 503 conversation persistence load failed. retry without conversation_id to start a fresh thread, or pass a different id. (.retryable: true) SCHEMA_DRIFT 503 conversation table is missing columns the platform expects. Same recovery as CONV_HISTORY_UNAVAILABLE — our team alerted, no caller action. PAID_API_NOT_ACTIVATED 402 caller needs to enable AI completions in the dashboard. .data.activation_url carries the link. INSUFFICIENT_BALANCE 402 monthly cap or pre-paid balance hit. .data has available_dollars + required_dollars + load_url. AI_SPEND_CAP_EXCEEDED 402 thrown only by chatWithTools when the running loop crosses opts.maxSpendCents. .metrics has the partial accounting. AI_MAX_ITERATIONS 422 thrown only by chatWithTools. Loop hit maxIterations without a terminal answer. .metrics.last_response is the final intermediate state. AI_REQUIRED 401 thrown by sw.auth.requireUser when no valid session is on the request. RATE_LIMITED 429 free-tier rate envelope hit or per-IP anonymous-create cap. (.retryable: true) UPSTREAM_ERROR 502/503 the upstream provider rejected. .message is the platform-authored friendly string; raw provider details are server-side only. VALIDATION_ERROR 400 inputs failed shape/range check. PROJECT_NOT_FOUND 404 unknown project or no access. NOT_FOUND 404 resource (conversation, etc) missing. CONFLICT 409 destination id already exists (e.g. conversations.fork to an id that's taken). INTERNAL_ERROR 500 unexpected platform failure — message stays generic, server logs have detail. Database-layer errors that surface through sw.db.* keep their own set documented in platform_help({ topic: 'sw.db' }): SCHEMA_ERROR · SYNTAX_ERROR · TABLE_NOT_FOUND · CONSTRAINT_VIOLATION · WRITE_CONFLICT · DB_BUSY · QUERY_TIMEOUT. Anything marked retryable: true is safe to attempt again after a short backoff. Everything else is a programming/state error — fix the input or wait for the platform team. ## Catalog search tool — `sw.ai.catalogTool` Every chat app with a content catalog (restaurants, products, stations, articles) ends up writing the same SQL-LIKE-over-N-columns tool by hand. `sw.ai.catalogTool` builds the tool definition AND the executor from a small config: const restaurants = sw.ai.catalogTool({ table: 'restaurants', searchColumns: ['name', 'cuisine', 'neighborhood'], resultColumns: ['id', 'name', 'cuisine', 'rating', 'image_url'], urlTemplate: '/restaurant/{id}', // optional; each result gets .url limit: 10, // optional, default 10, capped 50 // optional tool overrides: name: 'search_restaurants', description: 'Search restaurants by name, cuisine, or area.', // optional WHERE constraint (parameterized — never interpolated): where: { sql: 'is_published = ?', params: [1] }, }); // Drop into chatWithTools: const r = await sw.ai.chatWithTools({ model: 'claude-haiku-4-5', messages: [{ role: 'user', content: q }], tools: [restaurants.tool], async executeTools(toolCalls) { return Promise.all(toolCalls.map(async (tc) => { if (tc.name === restaurants.tool.name) { const out = await restaurants.execute(tc.input); return { tool_use_id: tc.id, content: JSON.stringify(out) }; } return { tool_use_id: tc.id, content: 'Unknown tool', is_error: true }; })); }, }); `tool` is the standard tool definition (works on every provider chatWithTools supports). `execute({query, limit?})` runs the SELECT and returns `{ count, query, limit, results: [...] }` with `.url` filled from the template when set. Identifier safety: table / column / tool names are validated against `[a-zA-Z0-9_]+` so the assembled SQL is injection-safe. The user's query string is bound as a parameter — never interpolated. ## Tool-use loop — `sw.ai.chatWithTools` Every chatbot ends up writing the same `while (stop_reason === 'tool_use')` loop with iteration caps, spend caps, and error handling. `chatWithTools` runs that loop for you — give it a tool dispatcher, it returns the final text plus iteration / cost metrics: const r = await sw.ai.chatWithTools({ provider: 'anthropic', model: 'claude-haiku-4-5', conversation_id: 'nibble:' + user.id, messages: [{ role: 'user', content: text }], tools: [ { name: 'lookup', description: '...', input_schema: {...} }, ], async executeTools(toolCalls) { // toolCalls = [{ id, name, input }, ...] return Promise.all(toolCalls.map(async (tc) => { try { const out = await runTool(tc.name, tc.input); return { tool_use_id: tc.id, content: JSON.stringify(out) }; } catch (err) { return { tool_use_id: tc.id, content: String(err), is_error: true }; } })); }, maxIterations: 5, // optional, default 5, max 20 maxSpendCents: 50, // optional, abort if running cost passes cap }); console.log(r.text); // final assistant prose console.log(r.iterations); // 1 = no tool calls; 2+ = tool round-trips console.log(r.tool_calls_made); // total tool_use blocks across all iters console.log(r.total_cost_cents); // sum across all iters (billing-equivalent) Throws on hitting `maxIterations` or `maxSpendCents` — error has `code` (`AI_MAX_ITERATIONS` / `AI_SPEND_CAP_EXCEEDED`) and a `metrics` field with everything spent so far so a logging handler can record the abort. Tool-runner exceptions are NOT fatal — they're fed back to the model as `tool_result` with `is_error: true` so it can recover within the same loop. With `conversation_id` set, the platform's normal history-replay covers each iter's prior turn — pass only the new user message, not the full history. Without `conversation_id`, the helper keeps an in-memory history for the duration of the call. Use `sw.ai.scoped(userId).chatWithTools(...)` to auto-scope the conversation by app-user. ## Per-user memory — `sw.ai.userMemory` Every chat app rolls its own `user_memory` table — Nibble has `nibble_memory`, RailTime would have `railtime_memory`. The platform now provides one: const m = await sw.ai.userMemory.get(user.id); // → {} on first read, or your stored blob await sw.ai.userMemory.update(user.id, { preferred_line: 'Northern' }); // → merges patch into the blob (shallow merge) await sw.ai.userMemory.clear(user.id); Storage lives in the project's own database as `_ai_user_memory` — `_`-prefixed so it's hidden from `db_browse` / `db_describe`. One row per user, free + unlimited. ## Auto-compaction — `sw.ai.userMemory.compact` After N conversation turns, fold the recent history into the structured blob via a single cheap-model call. Pass a JSON Schema and the platform uses `response_schema` to extract the structured output, then merges the result into the blob: await sw.ai.userMemory.compact(user.id, { type: 'object', properties: { preferred_line: { type: 'string' }, commute_time: { type: 'string' }, last_seen_disruptions: { type: 'array', items: { type: 'string' } }, }, }, { conversation_id: 'railtime:' + user.id, // pulls last N turns windowMessages: 20, // optional, default 20 model: 'claude-haiku-4-5', // optional, default Haiku maxTokens: 1024, // optional, default 1024 }); The compaction prompt carries forward existing memory unless the transcript contradicts it — so the call is safe to repeat. One `ai.chat` call's worth of cost per compact; run it on a cron or after every N user turns rather than per-turn. ## Structured output — `response_schema` Pass a JSON Schema object as `response_schema` to get a validated, parsed response. The platform injects a synthetic tool with that schema as its `input_schema` and forces the model to call it — works on both `provider: 'anthropic'` and `provider: 'openai'`. Mutually exclusive with caller-provided `tools` and with `stream`. const r = await sw.ai.chat({ provider: 'anthropic', messages: [{ role: 'user', content: `Extract order: ${text}` }], response_schema: { type: 'object', properties: { order_id: { type: 'string' }, customer: { type: 'string' }, amount: { type: 'number' }, }, required: ['order_id', 'customer', 'amount'], }, }) if (r.parsed) { console.log(r.parsed.order_id, r.parsed.customer, r.parsed.amount) } else { console.warn('parse failed:', r.parse_error) } The response gains two fields: `parsed` (the validated object, or null on failure) and `parse_error` (null on success, otherwise a short reason string). On validation failure the platform retries once silently before giving up — no exceptions are thrown so your handler can branch. ## Free models (workers-ai provider) Default pick — use a fast, non-reasoning model unless you specifically need reasoning; 1024 max_tokens is plenty for most prompts. A free model like `@cf/meta/llama-4-scout-17b-16e-instruct` is a good start. Call `ai_catalog` for the live list of models and their categories — don't hardcode a model list, it rots. ⚠️ Reasoning models — read this before picking one. (`ai_catalog` marks which models are reasoning models.) These models emit a private chain-of-thought before the final answer, which routinely consumes 2000–6000 output tokens BEFORE the visible reply even starts. With the typical 800–1024 max_tokens budget, they exhaust the budget mid-thought and return an UPSTREAM_ERROR that says "the model exhausted its output budget while reasoning." If you use a reasoning model, set max_tokens: 4000 or higher. For simple tasks such as summaries, classification, short answers, and chat replies, do not use a reasoning model. Use a non-reasoning model when hidden reasoning tokens are not required. Reach for reasoning models only on multi-step problems (math, code generation with planning, complex tool-use loops) where the extra thinking measurably improves the answer. Rate limits (per user, on free `provider: 'workers-ai'` models): Free tier: 10 req/min, 200 req/day Builder tier: 200 req/min, 10,000 req/day ## Other AI surfaces — also on sw.ai Embeddings, transcription, text-to-speech, image generation, and background removal are first-class on sw.ai inside a deployed function. No API key, no fetch, no project_id juggling — same pattern as sw.ai.chat. // Embeddings — a free model (free tier eligible) or a premium model const r = await sw.ai.embeddings({ provider: 'workers-ai', model: '@cf/baai/bge-base-en-v1.5', input: ['How do I reset my password', 'Billing help article'], }) // r.embeddings = [[...], [...]], r.dimensions = 768 // Premium embeddings — paid (rate at /v1/pricing) // Models: text-embedding-3-large (3072d, 13.0¢/Mtok), // text-embedding-3-small (1536d, 2.0¢/Mtok) // Optional dimensions param truncates the vector. const r2 = await sw.ai.embeddings({ provider: 'openai', model: 'text-embedding-3-large', input: ['...'], dimensions: 1024, // optional, must be <= model native }) // Transcribe (Whisper) — pass base64 audio or a public audio_url const t = await sw.ai.transcribe({ audio_url: 'https://example.com/clip.mp3', }) // t.text, t.duration_seconds // Generate an image — returns a raw Response unless you set storage const img = await sw.ai.generateImage({ provider: 'workers-ai', model: '@cf/black-forest-labs/flux-1-schnell', prompt: 'A serene mountain lake at dawn', }) return img // streams the PNG straight to the browser // …or store it in the project's files and return the path: const stored = await sw.ai.generateImage({ prompt: '...', storage: '/renders/cover.png', }) // stored = { storage_path, size_bytes, content_type } // Remove background — same storage / inline shape as generateImage const cut = await sw.ai.removeBackground({ image_url: 'https://example.com/photo.jpg', storage: '/cutouts/photo.png', }) // Browse the live model catalog const catalog = await sw.ai.catalog() ## Image generation models - @cf/black-forest-labs/flux-1-schnell — provider: 'workers-ai', fast, free - fal-ai/nano-banana — premium quality. Pass provider: 'fal', model: 'fal-ai/nano-banana', prompt, optional width/height. ## Background removal POST /v1/ai/remove-background with { project_id, image_url }. Backed by fal-ai BiRefNet, 1¢/image. Returns { url, content_type, bytes } — the output PNG is stored under the project's files. ## Text-to-speech - @cf/myshell-ai/melotts — workers-ai, ~0.02¢/min, default - grok-tts — premium voice (pricing at /v1/pricing). Voice options (voice param): eve, ara, rex, sal, leo. language defaults to 'en'. Max 15,000 chars per call. // Stream the audio straight to the browser const audio = await sw.ai.tts({ model: 'grok-tts', voice: 'eve', text: 'Hello world.', }) return audio // raw audio Response — Content-Type set to the right MIME // Or save to file storage and return its path const stored = await sw.ai.tts({ model: 'grok-tts', voice: 'eve', text: 'Hello world.', storage: '/audio/greeting.mp3', }) // stored = { storage_path, size_bytes, content_type, ... } ## Content moderation sw.ai.moderate(text) classifies text against a safety taxonomy (violence, sexual content, hate, self-harm, illegal advice, etc.). Free — uses the platform's content-safety model, with a secondary moderation fallback if the primary provider is down. const result = await sw.ai.moderate(userMessage) // { // flagged: true, // categories: ['violent_crimes', 'hate'], // scores: { violent_crimes: 1, hate: 1 }, // model: '@cf/meta/llama-guard-3-8b', // provider: 'workers-ai', // } if (result.flagged) { return Response.json({ error: 'Message blocked' }, { status: 422 }) } Categories returned: violent_crimes, non_violent_crimes, sex_crimes, child_exploitation, defamation, specialized_advice, privacy, intellectual_property, indiscriminate_weapons, hate, self_harm, sexual_content, elections, code_interpreter_abuse. Max input: 50,000 chars. Subject to the free-tier AI rate limits above. ## Catalog discovery sw.ai.catalog() (or the ai_catalog MCP tool / GET /v1/ai/catalog externally) returns every model the platform exposes — provider, family (chat / embeddings / tts / image / background-removal), pricing, and default + max steps. Every paid model carries the same flat platform markup (rate at /v1/pricing). Use it to render a picker or audit costs without hard-coding model lists. Use sw.ai.* inside any deployed function — the platform binding handles auth and project scoping. The smt_ developer key is only for external clients (CI/CD, server-to-server jobs, webhooks). ## Error envelope (typed catalogue) Every sw.ai.complete / sw.ai.chatWithTools failure surfaces as a structured envelope: ```json { "ok": false, "error": "", "message": "", "retry": true | false, "retry_after_ms": } ``` `retry: true` means the SAME request shape can be sent again after `retry_after_ms` (or immediately when omitted). `retry: false` means something about the request itself has to change before retrying. Match on `error` — the codes below are stable; the `message` text is not. | Code | retry | When you see it | |------------------------------|--------|-----------------| | VALIDATION_ERROR | false | bad input shape (missing project_id, bad messages array, unknown model, mutually-exclusive flags) | | PROJECT_NOT_FOUND | false | project_id doesn't exist or your key can't see it | | PAID_API_NOT_ACTIVATED | false | the paid model requires activation in dashboard settings | | CONV_HISTORY_UNAVAILABLE | true | the project DB couldn't replay prior turns. Retry without conversation_id to start a fresh thread | | CONV_HISTORY_REJECTED | true | the upstream provider rejected the replayed history for this conversation_id. The conversation is poisoned until cleared — call `ai.conversations.delete(id)` then retry, or drop conversation_id on the retry to start fresh | | RATE_LIMITED | true | per-user or per-model rate limit. Honour `retry_after_ms` | | UPSTREAM_RATE_LIMITED | true | upstream provider rate-limited us. Honour `retry_after_ms` | | UPSTREAM_AUTH_FAILED | false | platform credentials with the provider are bad — our team rotates them | | UPSTREAM_BILLING | false | platform has a billing problem with the provider — our team is notified | | UPSTREAM_DOWN | true | provider 5xx. Retry with backoff | | UPSTREAM_ERROR | false | provider 4xx (request rejected) — change your messages/tools | | INTERNAL_ERROR | true | platform glitch — retry once, then escalate via platform_feedback | Default retry behaviour for new codes follows the same rule: 4xx ⇒ `retry: false`, 5xx + 429 + 504 ⇒ `retry: true`. Always read `retry` instead of inferring from `error` — a future code may flip retryability without renaming. --- ## sw.email — Send Email (inside deployed functions) (sw.email) Requires Builder plan. Requires verified sender domain. await sw.email.send({ to: 'alice@example.com', from: 'notifications@yourapp.com', subject: 'Your order shipped', html: '

Tracking: ABC123

' }) The from domain must match a verified sender domain on this project. Cannot send from somewhere.tech or somewhere.site domains. Limit: 1,000 emails/day per project on Builder plan. For auth emails (password reset, verification, welcome), the platform sends them automatically — you don't call sw.email for those. Customize templates in Settings → Email Templates. ## Delivery health (scrub bounces before sending) The email provider posts bounce + complaint webhooks; the platform records every event on the project automatically. Two MCP tools expose them: - email_events_list({ project_id, limit?, offset? }) — recent sends with their latest event (sent | delivered | opened | clicked | bounced | complained | delivery_delayed). Build an audit view or spot a bounce spike after a blast. - email_bounces_list({ project_id, days?, limit? }) — deduplicated recipients that bounced or complained inside the window (default 30 days, max 365). One row per address with last_status, last_at, occurrences. For a single message's full timeline use email_status({ id }). **Before every send loop, scrub against email_bounces_list.** Repeated sends to bounced or complained addresses damage delivery health. Pattern: const bounced = new Set( (await ctx.email.bouncesList({ days: 60 })).map(r => r.address) ) for (const u of users) { if (bounced.has(u.email)) continue await ctx.email.send({ to: u.email, ... }) } --- ## sw.env — Environment Variables (inside deployed functions) (sw.env) Set env vars via the env MCP tool or the dashboard Settings page. They are baked into the deployed function bundle at deploy time. ## Access export default async function(req, sw) { const key = sw.env.STRIPE_SECRET_KEY // ... } ## Timing Setting an env var does NOT hot-reload running functions. After env_set, the next project_deploy bakes the new value in. Until you redeploy, old functions continue using the previous value. ## Secrets Env vars are server-side only — they never ship to the browser bundle. Put API keys, database URLs, and third-party secrets here. Rotate secrets by calling env_set with the new value, then project_deploy. --- ## sw.jobs — Background Jobs (inside deployed functions) (sw.jobs) Queue work that shouldn't block a user request. Platform retries on failure (at-least-once, up to 5 attempts). Make handlers idempotent — they may run more than once. ## Create a job const job = await sw.jobs.create({ handler: '/api/jobs/process-upload', payload: { upload_id: 'u123', user_id: 'u_alice' }, timeout_seconds: 600 }) // job = { job_id: 'j_abc', status: 'queued' } The handler must be a deployed function path. Platform POSTs the payload to your function when the job runs. ## Job handler function // api/jobs/process-upload.ts export default async function(req, sw) { if (!(await sw.jobs.verifyInvocation(req))) { return Response.json({ error: 'forbidden' }, { status: 403 }) } const { upload_id, user_id } = await req.json() // do the heavy work await processUpload(upload_id, sw) return Response.json({ ok: true }) } The platform signs each job delivery with X-Somewhere-Signature, X-Somewhere-Invocation-Timestamp, and X-Somewhere-Body-SHA256. Always verify before trusting the payload on public /api/jobs/* handlers. This blocks a browser from calling the job handler directly with forged payloads. ## Look up a job's status const j = await sw.jobs.status('j_abc') // j = { job_id, status: 'queued' | 'running' | 'succeeded' | 'failed', // attempts, last_error?, started_at?, finished_at? } Use this to poll a long-running job from another request — for example, the browser polls /api/job-status?id=j_abc and your function calls sw.jobs.status to forward the current state. --- ## sw.queue — Fire-and-Forget Background Work (sw.queue) For fire-and-forget tasks where you don't need to inspect status later. Use sw.jobs if you DO need to look up the result. ## Push a message await sw.queue.push({ handler: '/api/queue/log-event', payload: { event: 'signup', user_id: 'u_123' }, delay_seconds: 0 // optional, max 900 (15 min) }) ## Handler function // api/queue/log-event.ts export default async function(req, sw) { if (!(await sw.queue.verifyInvocation(req))) { return Response.json({ error: 'forbidden' }, { status: 403 }) } const { event, user_id } = await req.json() await sw.db.query('INSERT INTO events (type, user_id) VALUES (?, ?)', [event, user_id]) return Response.json({ ok: true }) } Queue deliveries are signed the same way as jobs. Verify the signature before side effects so public queue handler URLs cannot be used as unauthenticated admin endpoints. ## Delivery semantics At-least-once — a consumer MAY receive the same payload twice. Make handlers idempotent (e.g. dedupe key on a unique column). No per-message read API — if you need to look up a job by id, use sw.jobs. --- ## sw.logs — Application Logging (inside deployed functions) (sw.logs) Structured logs written to the project's log stream. Readable via the log MCP tool or dashboard Logs page. ## Write logs sw.logs.debug('loaded config', { keys: Object.keys(config) }) sw.logs.info('user signed up', { user_id: 'u_123' }) sw.logs.warn('slow query', { ms: 812, sql: 'SELECT ...' }) sw.logs.error('payment failed', { error: err.message, user_id: 'u_123' }) All four levels accept (message: string, data?: object). data is JSON-serialized, so keep it under a few KB per call. ## Retention Free: 7 days. Builder: 30 days. A daily cron prunes older entries — there is no archive. ## Reading logs Call the log MCP tool with project_id + optional level + limit. No full-text search across log bodies — log retrieval is sequential (newest first, optionally filtered by level). --- ## Deploy (deploy) ## Reliability guarantees on every deploy (capability list) - **Deploy health check (synthetic smoke test).** After every promote the platform screenshots the live root URL; if the screenshot is blank / under threshold the deploy is REJECTED, not promoted. No "two-second white screen" outage. - **Version history + rollback.** Every deploy writes a versioned snapshot. `project_rollback` restores files + functions + schema-state to any prior version in one call. Customer-facing rollback latency is ~5 seconds. - **Canary deploys** for the platform itself: the API worker rolls every new version to 10% of traffic, our team watches for 5–15 minutes, then promotes. Roll-back to the previous version is one command. - **Continuous code review (LLM-driven, daily + per-deploy).** A static analysis (SAST) scanner runs at build time, plus an LLM-driven review (DAST-style end-to-end) inspects every deploy AND re-scans every project nightly. Findings are written to the dashboard's Security tab and alert our team. - **Failure alerting** on deploy / promote / patch / blank-page / health checks. Our team is alerted to any regression within minutes. Full reviewer-facing depth: . --- **Ship raw source. The platform compiles JSX/TSX/TS automatically — for both the frontend AND the backend.** `/v1/deploy` compiles your sources at deploy time: - Frontend: `.jsx` / `.tsx` / `.css` are bundled into hashed chunks under `/_compiled/`, and `index.html` is rewritten to point at them. - Functions: each `api/*.ts` (or `.tsx`, `.js`) is compiled to JS the runtime can execute — no client-side `tsc`, no `esbuild api/chat.ts`, no pre-bundled `.mjs`. You ship the same files you write. Do not run `npm run build`, `vite build`, `esbuild`, `tsc`, or similar first — `/v1/deploy` HARD-REJECTS pre-bundled output with HTTP 400 `BUNDLED_DEPLOY_REJECTED`. This includes pre-bundled `api/*.mjs` functions, not just `dist/` static output. Why the functions side matters: the compiler bundles each function as its own entry with cross-import resolution. If you pre-bundle `api/chat.ts` with esbuild into `api/chat.mjs`, the resulting file typically contains an `import './webhook'` that points at a sibling that doesn't exist as a runtime module → 500 at first request. Ship the `.ts` source and the platform's compiler handles cross-imports. Escape hatch (legacy build pipeline, native binary, etc): - Single deploy: pass `{ allow_bundled: true }` in the body. - Whole project: ask support to flip `allow_bundled = 1` on the project. Existing projects are grandfathered; new ones default off. Two ways to ship code: project_deploy — full replacement, use for the initial deploy or a major rebuild project_patch — incremental, use for small edits, single-file changes, adding one endpoint **Every deploy goes live.** There is no separate dev/prod step — both `project_deploy` and `project_patch` write to both serving slots in lockstep. The result is immediately at `https://{subdomain}.somewhere.tech` and on any verified custom domain. `?env=dev` is preserved as an internal-only opt-in for testing the same bytes from the dev slot. The promote step is gone; `project_rollback` is still available for reverting to a previous version. ## project_deploy — full replacement Every project_deploy call replaces ALL files and ALL functions with exactly what you send. There is no partial / incremental update through this tool — use project_patch for that. If you deploy this: { files: { "index.html": "..." } } ...then deploy this next: { files: { "about.html": "..." } } ...index.html is GONE. Only about.html exists. To update one file through project_deploy you must include EVERY file you want live in the same call. For small edits, reach for project_patch instead. ## project_patch — incremental One file per call. Two modes, same tool — pick the smaller payload each time. Unchanged files are always preserved server-side. Static-file changes go live in ~1 second; function changes in 2–4 seconds. **Find / replace (~200 bytes on the wire — preferred for small edits):** project_patch({ project_id: "my-app", path: "index.html", find: "

Pricing

", replace: "

Plans & pricing

" }) Replaces every occurrence of `find` with `replace`. If `find` doesn't match anywhere in the file, you get a 400 `FIND_NOT_FOUND` with a snippet of what the file currently says — retry with the right substring. This is the token-optimal path for visual-editor iteration (Claude Design, Cursor). **Full content (rewriting the file):** project_patch({ project_id: "my-app", path: "api/hello.ts", content: "export default async (req, sw) => Response.json({ v: 2 })" }) Paths are auto-routed: anything under `api/` or `_lib/` (or a root `[id].ts`-style parametric route) is a function; everything else is a static file. **Binary assets** (images, fonts) — `content` and `find`/`replace` operate on text. Use the binary write surface (`update_binary_files`) for raw bytes; trying to ship them through this tool's text fields corrupts them via UTF-8. **Delete:** `delete_files` is an array of paths. Works for both static files and functions; missing paths are silently ignored. **Conflict check (optional, recommended for shared projects):** pass `expected_version` from your last deploy/patch response. If another deployer landed changes since, returns 409 `VERSION_CONFLICT` with `data.current_version` instead of overwriting. Omit to skip. Everything you don't name stays untouched. What is NOT affected by a deploy: - Env vars (managed by env_set) - Database contents (managed by db_query, db_migrate) - Uploaded files written via fs_write / sw.fs (separate storage) ## Example project_deploy({ project_id: "my-app", files: { "index.html": "...", "styles.css": "body { color: red }" }, functions: { "api/hello.ts": "export default async (req, sw) => Response.json({ ok: true })" }, binary_files: { "images/logo.png": "iVBORw0KGgo..." } }) files = static assets served as-is (HTML, CSS, JS, images as text) functions = server-side code with sw access, routed by file path binary_files = images/fonts/PDFs as base64, decoded on the server ## Rollback `project_rollback` reverts the last deploy — customers immediately see the previous working version. Use it when a deploy broke something. `project_deploys` lists prior numbered versions; `project_restore_version` restores a specific one. The last 10 are kept. ## After every deploy, the platform automatically: 1. **LLM security review** (premium, Builder+) — an LLM pass over your deployed source returns structured findings against 9 risk categories (auth bypass, raw SQL, unsafe payment metadata, env leaks, privilege escalation, RCE, email spoofing, conversation hijack, CSRF). On-demand: → `security_review({ project_id })` or `POST /v1/security/review` after promote. 2. **Screenshots the live site** — desktop + mobile, captured per version. The dashboard uses these as previews. → `project_screenshots({ project_id })` or `GET /v1/deploy/screenshots`. 3. **Generates a Mermaid architecture diagram** — rendered inline on the project Overview. → `GET /v1/deploy/architecture`. 4. **Writes a plain-English description of what your app does** — shown under the project name on the dashboard. → `GET /v1/deploy/description`. Screenshots, architecture, and description run after the deploy response returns. The LLM security review runs on-demand today — call `security_review` after a deploy. (Planned: an automatic run on promote.) Separately, a quick regex scanner also runs during compile and surfaces obvious unsafe patterns as advisory `warnings` in the deploy response — cheap synchronous gate, every tier. Full reference: `platform_help({ topic: 'deploy-intelligence' })`. --- ## Deploy intelligence — what happens after every deploy (deploy-intelligence) Every `project_deploy`, `project_patch`, and `project_rollback` gets four post-deploy outputs: 1. **LLM security review** — an LLM pass over your deployed function source that returns structured findings. Premium feature (Builder+). Run via the `security_review` MCP tool or `POST /v1/security/review` after promote. 2. **Screenshots** — desktop + mobile PNGs of the live homepage, captured automatically. 3. **Architecture diagram** — Mermaid flowchart of the project. 4. **Plain-English description** — one-paragraph summary of what the app does. Screenshots, architecture, and description fire automatically in a post-promote async block — they're ready a few seconds after the deploy response returns. The LLM security review runs on-demand today — call it after a deploy. (Planned: an automatic run on promote.) Separately, every deploy also runs a quick regex scanner during compilation. It catches obvious unsafe patterns like authenticated handlers calling raw `sw.db.query(...)` without `{ user }` for owner-scoping, and surfaces hits as advisory `warnings` in the deploy response. It is NOT the security review — it's the cheap synchronous gate that ships to every project at every tier. Suppress per-file with a `somewhere-security-allow` comment. ## 1. LLM security review (premium) An LLM pass reads up to 40 of your deployed function files (8 KB each) and returns Markdown findings against 9 risk categories: 1. **Auth bypass** — public routes trusting caller-supplied user_id / session / role instead of `sw.auth.fromRequest(req)`. 2. **Raw SQL exposure** — functions concatenating user input into query strings. 3. **Unsafe payment metadata** — `sw.payments.checkout` called with caller-controlled metadata or userId. 4. **Env leakage** — handlers returning `sw.env.*` values in responses. 5. **Privilege escalation** — `sw.auth.admin`, `sw.keys`, `sw.deploy`, `sw.db.migrate` called from public handlers. 6. **RCE risk** — `eval`, `new Function`, dynamic import of user-controlled strings. 7. **Email spoofing** — `sw.email.send` with `from:` derived from request input. 8. **Conversation hijack** — `sw.ai.complete` with `conversation_id` from caller-controlled input without an ownership check. 9. **CSRF / origin checks** — destructive POSTs without origin or token verification. Each finding: WHAT (one line) · WHERE (file + region) · IMPACT (one line) · FIX (snippet or one sentence). Plus a "Risk summary" paragraph at the end. // MCP security_review({ project_id: "my-app", focus: "the new payments flow" }) // → { findings, model, version, files_shown, files_total, usage, tier_unlocks_deep_review } // REST POST /v1/security/review { "project_id": "my-app", "focus": "..." } Tier behavior (`features.llm_security_review`): - **Free** — LLM security review unavailable. Screenshot and other non-LLM deploy intelligence surfaces remain available as documented. - **Builder / Pro / Scale / Enterprise** — review available according to tier configuration. Review depth and output caps are plan-defined. Advisory only — never blocks anything. Standalone feature, not a deploy gate. ## 2. Screenshots After the deploy commits, the platform launches a headless browser and screenshots the live homepage at two viewports in one browser launch: - desktop: 1280 × 800 - mobile: 375 × 667 (iPhone SE class) Captured from the project's custom domain if one is attached, otherwise the `*.somewhere.tech` subdomain. // MCP project_screenshots({ project_id: "my-app" }) // REST GET /v1/deploy/screenshots?project_id=my-app[&version=42] The screenshot also backs the blank-page gate — a PNG below 10 KB suggests the page isn't rendering, which surfaces as a deploy alert (see `platform_help({ topic: 'deploy' })` for the gate). ## 3. Architecture diagram A Sonnet 4.5 pass writes a Mermaid flowchart of the project from the function source. The dashboard fetches it and renders client-side with the `mermaid` package. GET /v1/deploy/architecture?project_id=my-app[&version=42] // → { project_id, version, mermaid, generated_at } // Force a regenerate without redeploying: POST /v1/deploy/architecture/regenerate { "project_id": "my-app" } An empty `mermaid` string means no diagram has been generated yet — clients should hide the tile in that case. ## 4. Auto-description A Haiku call reads the project's source and structure, then writes a short plain-English summary of what the app does: > "Restaurant discovery and booking app with AI chatbot. 89 users > browse 24 restaurants and book tables." The dashboard renders it under the project name on the overview card so the project can be identified without reading source. GET /v1/deploy/description?project_id=my-app[&version=42] // → { project_id, version, text, generated_at } An empty `text` means no description yet (newly-created project, never promoted, or the AI call failed). Clients should fall back to the user's type-it-yourself description. ## Surface coverage | Output | MCP tool | REST endpoint | | --------------------- | ----------------------- | ---------------------------------- | | LLM security review | `security_review` | `POST /v1/security/review` | | Regex deploy scanner | (in deploy warnings) | (in deploy response) | | Screenshots | `project_screenshots` | `GET /v1/deploy/screenshots` | | Architecture | `project_architecture` | `GET /v1/deploy/architecture` | | Description | `project_description` | `GET /v1/deploy/description` | All four outputs are now callable from MCP. Agents typically pull `project_architecture` + `project_description` before editing an unfamiliar project — both are idempotent reads. ## Use cases - Dashboard version history can display live-site screenshots. - Project lists can display generated descriptions. - Reviewers and agents can inspect the generated architecture before editing source. - Deploy responses can surface deterministic guardrail warnings when source changes. --- ## Deploy serves live (dev-prod) The dev/prod split was removed in 2026-05. Every `project_deploy` and `project_patch` writes to both serving slots in lockstep — there is no separate promote step. The result lands at: https://{subdomain}.somewhere.tech (default, what your users hit) https://yourcustomdomain.com (if a custom domain is verified) `?env=dev` is preserved as an internal opt-in for testing the same bytes from the dev slot — same code as prod, but useful for cache busting during testing. Not advertised to end users. Workflow: 1. Deploy changes → live immediately on the bare URL and any custom domain 2. Bug found? `project_rollback` → reverts to the previous deploy ## Conflict check (collaborated projects) Every project_deploy / project_patch returns `version` — a monotonic counter that bumps on each successful deploy or patch. Pass it back as `expected_version` on the next call and the platform returns 409 `VERSION_CONFLICT` if another deployer landed changes since: // First deploy const r1 = await project_deploy({ project_id, files }); // r1.version === 5 // Later, after editing locally const r2 = await project_deploy({ project_id, files, expected_version: 5 }); // If a collaborator deployed in the meantime, r2 returns: // { ok: false, error: "VERSION_CONFLICT", // data: { current_version: 6, expected_version: 5 } } // → Re-fetch the latest project state, merge, redeploy with // expected_version: 6. The check is opt-in. Single-deployer projects can omit `expected_version` and the deploy always succeeds. --- ## Inspect — Read live state before writing (inspect) You open a session and the user says "fix the auth bug in adapted-co" or "add a /pricing page to my-saas". The local workspace may be stale, empty, or out-of-date — and another agent (or the user themselves on another machine) may be editing the same project right now. Inspecting before writing is the difference between a clean patch and overwriting someone else's work. ## The default inspect-first order 1. project_get { project_id } → Confirms the project exists. Returns subdomain, current code `version`, custom domain, owner. Save the version — it is your `expected_version` floor. 2. project_export { project_id } → Pulls the live deployed source (static + functions) in one call. The platform is the authoritative copy. (CLI equivalent: `somewhere pull ` writes it to .//.) 3. project_deploys { project_id } → Last 20 versions with timestamps and deploy messages. Look for: deploys in the last hour (someone is active), gaps between versions (interrupted shipping), unfamiliar messages. 4. deploy_status { project_id } → Confirms the mirrored dev/prod serving slots are in sync. Drift here means a previous deploy was interrupted — stop and resolve before piling new changes on top. For runtime symptoms, layer on: errors { project_id } → Last 50 runtime errors with stack and request path. Run this BEFORE assuming the bug is wherever the user pointed you. log { project_id, function_path?, since? } → console.log output from deployed functions. Filter by function path or time window when investigating a specific endpoint. project_diff_versions { project_id, from, to } → Per-file delta between two preserved versions: added, removed, changed (with truncated text previews), plus function source. Use after a VERSION_CONFLICT to see what the other agent shipped before merging and retrying. fs_versions { project_id, path } → History for a single user-uploaded file (anything written under /v1/fs/*). NOT for project code — see below. ## Two version namespaces, never confused There are TWO independent histories. Picking the wrong one is a common mistake. Code history: project_deploys / project_restore_version Covers files + functions shipped by project_deploy or project_patch. Upload history: fs_versions / fs_restore Covers user-uploaded files under /v1/fs/* — uploads, generated PDFs, cached blobs. Each path has its own version chain. `project_restore_version` rolls back code but leaves uploaded data alone. `fs_restore` rewinds one uploaded file without touching code. Pick the right tool for what is broken. ## Editing what you pulled After `project_export`, the response includes the current deploy `version` on each pulled project. Hold that number. Pass it as `expected_version` on every subsequent `project_deploy` / `project_patch` against this project. const exported = await project_export({ project_id: "adapted-co" }); // exported.version === 47 // ...edit files locally... await project_patch({ project_id: "adapted-co", path: "api/auth.ts", content: "...", expected_version: 47, }); If another agent deployed between your clone and your patch, the platform returns: { ok: false, error: "VERSION_CONFLICT", data: { current_version: 48, expected_version: 47 } } To see what they changed BEFORE retrying: await project_diff_versions({ project_id: "adapted-co", from: 47, // your expected_version to: 48, // current_version from the 409 }); Merge their changes into your local copy, then retry with `expected_version: 48`. ## Subscribe to deploys in real time Every successful `project_deploy` / `project_patch` / `project_restore_version` publishes a message on the `system:project` realtime channel for that project. Subscribe from inside a deployed function or with the realtime MCP tool to be notified the instant another agent ships: // From a deployed function sw.realtime.subscribe("system:project", (msg) => { if (msg.event === "deployed" || msg.event === "patched") { console.log("New live version:", msg.data.version, "by", msg.data.by); } }); Event shape: { event: "deployed" | "patched" | "restored" | "rolled_back", data: { version: 49, by: "user_…", // actor user_id message: null, // optional deploy message has_functions: true, at: "2026-05-12T18:04:11.000Z", } } A long-running editing agent should subscribe to this channel and treat any `event: "deployed"` with a higher version than its held `expected_version` as "stop, re-clone or diff, then retry." ## Who deployed v13? `project_deploys` now returns each row with a `deployed_by` object containing the actor's `user_id` and `email`. NULL means the row was written before actor tracking was added (the project owner is the safe fallback in that case). ## When you can skip the inspect-first dance - You just created the project in this session. - The user says "I'm the only person editing this." - You are only reading (db_query, log, errors). Inspect before writing whenever: - The user mentions another machine, another agent, or a collaborator. - `project_deploys` shows multiple deploys today. - You see "I have local edits" or "I cloned this last week". - The last `project_deploy` response is more than ~10 minutes old in your conversation — assume someone else may have shipped since. ## Remaining limits (be honest with the user) - `project_deploy` is full-replacement (every file you send replaces the live set). There is no dry-run or three-way merge — preserving anything you want to keep is the caller's job. - Single-file optimistic concurrency for `fs_write` does not exist yet — `expected_version` is a project-code check, not a per-file CAS for uploads under /v1/fs/*. When either of those limits matters, surface it to the user instead of pretending the platform has the feature. --- ## Custom Domains (domains) There are four flavors of custom domain. Pick by what the user is doing: Claimed domain — RECOMMENDED for any domain the user already owns. They delegate nameservers to us once; the domain can then be attached to (and detached from) any project, plus it unlocks inbox routing on the same domain. Custom domain — legacy CNAME-only flow. One project, one hostname, apex requires ALIAS support at the registrar. Use only when the user can't change nameservers. Tenant domain — one hostname per CUSTOMER of your app. Every request on the hostname is dispatched to your prod function with the original Host header preserved; your code branches on req.headers.get('host') to render that tenant's view. Use for white-label / multi-tenant SaaS. Bought domain — registered through the platform via Name.com. After purchase the platform also wires DNS + SSL, so the domain comes online without a separate domain_add call. ## Recommended: claim by delegating nameservers This path covers apex (`example.com`), `www`, and any subdomain in one move. Once active, the domain can be re-attached to a different project without re-verifying ownership. // 1. Claim — returns the nameserver pair to paste at the registrar. domain_claim({ domain: "example.com" }) // → { id: "dom_...", claim_status: "pending_ns", // nameservers: ["ada.ns.cloudflare.com", "kirk.ns.cloudflare.com"], // instructions: [...] } // 2. User updates nameservers at registrar (Namecheap, GoDaddy, ...). // 3. Either wait for the hourly poll, or check on demand: domain_check_ns({ id: "dom_..." }) // → { claim_status: "active" } once propagation is complete. // 4. Attach to a project. host = "apex" (default), "www", or a // single subdomain label like "app". domain_attach({ id: "dom_...", project_id: "my-app", host: "apex" }) // To move the domain to a different project later — no re-verify: domain_detach({ id: "dom_..." }) domain_attach({ id: "dom_...", project_id: "other-app", host: "apex" }) // To list every domain the user has claimed (regardless of attachment): domain_owned({}) ## Inbox on a claimed domain (opt-in) `domain_claim` deliberately leaves MX records alone — the user might already receive email at the domain via Gmail, Workspace, or their own server. Turn on inbox routing only when the user explicitly wants it: domain_enable_email({ id: "dom_..." }) // ⚠ Replaces existing MX records on the domain. // After this, inbox_create_address works on this domain. If the user already has email working, skip this and keep their MX intact. ## Stuck domain recovery If a claimed domain shows `claim_status: active` and `zone_status: active` in `domain_owned` but `dig ` returns NXDOMAIN or empty answers, the platform-wiring step had a partial failure during claim. Recover with: domain_publish({ id: "dom_..." }) This re-asserts the apex + www records and the routing. Idempotent — safe to call any time. Inbox routing is only re-asserted if it was already enabled, so this never silently overwrites MX records. ## Legacy: CNAME-only (custom_domain) For the rare user who can't change nameservers (e.g. corporate domain they don't own). Apex needs ALIAS / ANAME support at the registrar. domain_add({ project_id: "my-app", domain: "app.yourcompany.com" }) // Returns the CNAME + verification TXT to set at the registrar. domain_verify({ project_id: "my-app", domain: "app.yourcompany.com" }) // Polls DNS + SSL. Once verified, the domain serves prod immediately. A verified custom domain serves your latest deploy — same bytes as the `*.somewhere.tech` subdomain. ## Buy a domain (two-step — non-refundable) Domain purchases charge the user immediately and CANNOT be refunded, so domain_buy through MCP is gated behind a confirmation step. Always show the price to the user verbatim before calling domain_buy_confirm. Step 1 — Get the price + a confirm token: domain_check({ domain: "acmedash.com" }) // → { available: true, price: "/year" } domain_buy({ project_id: "my-app", domain: "acmedash.com" }) // → { ok: false, error: "PURCHASE_CONFIRMATION_REQUIRED", // domain: "acmedash.com", price: "/year", // confirm_token: "tok_abc123", // message: "...non-refundable. Token expires in 10 minutes." } Show the user the price. Get explicit go-ahead. Then: Step 2 — Charge + register + wire DNS/SSL automatically: domain_buy_confirm({ project_id: "my-app", domain: "acmedash.com", confirm_token: "tok_abc123" }) // → { domain: "acmedash.com", status: "active", dns_configured: true } The token expires in 10 minutes. If it lapses, call domain_buy again to mint a fresh one. The first paid domain on a Free account also unlocks 30 days of Builder for free. ## Tenant domains (one hostname per customer) domain_add_tenant({ project_id: "my-app", domain: "customer1.com" }) // → { cname_target: "proxy.somewhere.tech", cf_hostname_id, ssl_status } domain_verify_tenant({ project_id: "my-app", domain: "customer1.com" }) // SSL provisions automatically once DNS resolves. Inside your prod function, branch on the host header to render that tenant: // api/index.ts export default async function(req, sw) { const host = req.headers.get('host') const tenant = await sw.db.query( 'SELECT * FROM tenants WHERE hostname = ?', [host] ) return Response.json({ name: tenant.data[0].name }) } Tenant routes match every path (not just /api/*), unlike custom_domain which only routes /api/* to functions. Pick tenant when the customer should see your full app under their own brand; pick custom domain when you want one branded URL for your own app. ## Domain search // Search for available domains across many TLDs at once // (rate-limited to 10/min and 100/day per user; results are cached // for 5 minutes so the same keyword is instant after first hit) // → returned by the /v1/domains/search REST endpoint or the // somewhere.site /search page. ## Listing + removal domain_list({ project_id: "my-app" }) // custom domains domain_remove({ project_id, domain }) // detach a custom domain --- ## Realtime — Channels (publish / subscribe) (realtime) A channel-based publish/subscribe primitive. Use for chat, live dashboards, lightweight notifications — anything message-shaped that needs to reach connected clients in real time. Each (project_id, channel) pair routes to its own isolated session, so projects never share traffic. Idle channels hibernate automatically — you pay essentially nothing while nobody's connected. ## Channel names Match the regex [a-zA-Z0-9][a-zA-Z0-9_\\-:.]{0,127}. Pick a feature-prefixed format: chat:room-42, private:usr_123:notifications, live-scores:match-9 For per-user / private data, name the channel with a private:USERID: prefix (e.g. private:usr_123:notifications) — only that signed-in user can subscribe or publish to it. ANY other channel name (chat:room-42, notifications:u_123) is readable AND writable by every signed-in app user in the project, so never put one user's private data on a non-private channel. ## Publish from a deployed function (the common case) sw.realtime.publish auto-injects project_id and handles auth — no smt_ key, no fetch. await sw.realtime.publish('chat:room-42', { body: 'Hello', author: 'alice' }, { event: 'new_post' }) // → { channel, event, delivered } The 'event' option names the event ('message' by default). The data argument is any JSON value. Payload cap: 64 KB. ## Publish from CI / external (MCP) From an external process (CI, server-to-server worker, MCP agent), use the MCP tool — smt_-authorized: realtime_publish({ project_id: "my-app", channel: "chat:room-42", event: "new_post", data: JSON.stringify({ body: "Hello", author: "alice" }) }) ## Subscribe from a browser Channels are project-scoped. Mint an app-user JWT (sw.auth.login) and pass it as ?token=. Recommended: paste the swRealtime() helper below — it handles reconnect, backoff, and re-subscription on socket loss so you can focus on your event handler instead of WebSocket plumbing. // swRealtime — copy-paste browser helper (~40 lines, no dependencies). // Auto-reconnects with exponential backoff (1s → 30s, full jitter), // re-establishes the subscription on every reconnect, JSON-parses the // envelope, and never throws past your callbacks. Drop-in for both // project channels and the personal system:user channel. function swRealtime({ projectId, channel, token, userId, onMessage, onOpen, onClose, onError }) { let ws = null, stopped = false, attempt = 0 function connect() { if (stopped) return const base = 'wss://api.somewhere.tech/v1/realtime/' const url = userId ? base + 'subscribe-user?user_id=' + encodeURIComponent(userId) + (token ? '&token=' + encodeURIComponent(token) : '') : base + 'subscribe?project_id=' + encodeURIComponent(projectId) + '&channel=' + encodeURIComponent(channel) + (token ? '&token=' + encodeURIComponent(token) : '') ws = new WebSocket(url) ws.onopen = (e) => { attempt = 0; onOpen && onOpen(e) } ws.onmessage = (e) => { try { onMessage(JSON.parse(e.data)) } catch (err) { onError && onError(err) } } ws.onerror = (e) => { onError && onError(e) } ws.onclose = (e) => { onClose && onClose(e) if (stopped) return const cap = Math.min(30000, 1000 * Math.pow(2, attempt++)) setTimeout(connect, Math.floor(Math.random() * cap)) } } connect() return { close() { stopped = true; ws && ws.close() } } } // Usage — project channel (browser, app-user JWT): const sub = swRealtime({ projectId: 'my-app', channel: 'chat:room-42', token: jwt, onMessage: (env) => { // env = { type, event, data, from, at } if (env.event === 'new_post') render(env.data) }, }) // sub.close() // when the user navigates away // Usage — personal system:user channel (platform feedback responses, // cross-project events). Developer smt_ key only. swRealtime({ userId: currentUserId, token: smtKey, onMessage: (env) => { if (env.event === 'feedback_resolved') toast(env.data) }, }) The JWT's project_id is enforced to match the channel's project, so a user can never cross-project. On reconnect the platform does not replay buffered events — refetch state via REST inside onOpen if you can't tolerate gap-time losses. ## Server-side listener (webhook-style between functions) sw.realtime.subscribe long-polls the platform for the next event on a channel. Useful when one function publishes a result and another function (running on a request) wants to wait for it. const env = await sw.realtime.subscribe('jobs:export-done', { timeout_ms: 20000, event: 'done' }) if (env) handle(env.data) else respondPending() ## List active channels sw.realtime.channels() returns rows the platform has touched in the last 10 minutes — current subscriber count + last_publish_at. From an agent: realtime_channels({ project_id }). ## Limits Free: 100,000 publishes / month. Pro+: unlimited. Per-message payload cap: 64 KB. ## Platform-emitted system events (subscribe-only) The platform publishes its own lifecycle events on two reserved channels. You don't publish to these — just subscribe and let the platform push everything. realtime_subscribe_project({ project_id: 'my-app' }) // → { channel: 'system:project', websocket_url, event_types: [...] } realtime_subscribe_user({}) // → { channel: 'system:user', websocket_url, event_types: ['feedback_resolved'] } The 'system:project' channel emits: - 'deployed' | 'patched' | 'restored' | 'rolled_back' { version, by, message, has_functions } — multi-editor conflict prevention. See a version bump, pull before pushing. - 'db_health' { event: 'cpu_exhaust_recovered' | 'cpu_exhaust_failed', query_fingerprint } — fires when the platform retried a slow query for the user. - 'quota_warning' { resource: 'storage' | 'database' | 'email' | 'realtime' | 'ai' | 'inbox', usage_percent, message } — fires once per 80% and 95% crossing per resource per month. - 'auth_event' { event: 'user_deleted' | 'user_banned' | 'user_unbanned' | 'impersonation_started', user_id, actor_id }. The 'system:user' channel emits 'feedback_resolved' { ticket_id, response, resolution_status } when the platform team responds to or resolves a feedback() ticket you submitted. Auth: developer smt_ key only; the channel is bound to your own user id. Platform-emitted events bypass the publish quota — they're free. ## Don't build with this Durable history, replay-on-connect, presence, stateful rooms. Realtime is a fire-and-forget transport. State belongs in sw.db. --- ## Cron — Scheduled Tasks (cron) Run a handler on a fixed schedule. All times are UTC. ## Create a schedule cron_create({ project_id: "my-app", schedule: "0 9 * * *", // 5-field cron expression handler: "/api/jobs/daily-digest", payload: { segment: "daily" } // optional }) ## Schedule format minute hour day-of-month month day-of-week 0 9 * * * → every day at 09:00 UTC */15 * * * * → every 15 minutes 0 0 1 * * → first of every month ## Handler Point handler at a deployed function. Platform POSTs payload when the schedule fires. Same at-least-once semantics as sw.jobs. ## Missed fires If the platform is down across a fire time, the run is NOT replayed. Design schedules to tolerate an occasional skipped run. --- ## Smoke tests — auto-wired uptime checks (smoke) Every deployed project is probed automatically — no setup. Homepage on the project's `*.somewhere.tech` subdomain plus each claimed custom domain root, every ~20 minutes via cron and once after every deploy. Status per run: `pass` / `partial` / `fail`. A fail-after-pass transition is detected and alerted automatically, debounced by `alert_cooldown_seconds` (default 3600). ## MCP smoke_status({ project_id }) → { latest: SmokeRun, history: SmokeRun[] } smoke_run({ project_id }) → { run: SmokeRun } // synchronous on-demand probe smoke_config_get({ project_id }) smoke_config_update({ project_id, enabled?, test_auth_flow?, alert_cooldown_seconds?, extra_checks?, test_user_email?, canary_enabled? }) `extra_checks` is an array of additional URL probes: [ { name: "users-list", path: "/api/users", expect_status: 200 }, ... ] ## Probe fields Each probe accepts: - `path` (required) — must start with `/`. - `method` — default `GET`. - `name` — label in run results (max 80 chars); auto-generated if omitted. - `expect_status` — exact HTTP status. Without it, anything below 500 passes — useful for "POST /api/login with no body should be 4xx, not 500" and similar contract checks. - `headers` — extra request headers (max 10 per probe). - `body` — request body. An object is sent as JSON (Content-Type set for you); a string is sent raw. Max 8 KB. - `auth: "test_user"` — the probe runs authenticated: the platform mints a fresh session for the project's designated test user at probe time. Set the test user first with `smoke_config_update({ test_user_email })` — it must be an existing app user of the project (create one via signup). No credentials are ever stored in the manifest. When `auth` is set, the Authorization header can't be overridden via `headers`. - `expect_json_path` — dot-path (or array of them, max 10) that must resolve to a non-null value in the JSON response, e.g. `"data.reply"` or `"choices.0.message"`. - `expect_contains` / `expect_not_contains` — substrings the response text must / must not contain (string or array, max 10 each). - `expect_max_ms` — fail the probe if it takes longer than this, even on HTTP 200. ## 15-minute canary (opt-in) smoke_config_update({ project_id, canary_enabled: true }) Runs the full check set (homepage + custom domains + extra_checks + `/_smoke_tests.json`) every 15 minutes, on top of the regular cadence. Off by default. A fail-after-pass transition alerts the project owner (debounced by `alert_cooldown_seconds`). ## Auto-detected: `/_smoke_tests.json` Deploy a JSON file at `/_smoke_tests.json` in your project and the cron picks up the entries automatically — same shape as `extra_checks`, no config call needed. Up to 20 entries per project. Useful when you want the smoke contract to ship with the code, not sit in the dashboard: ```json [ { "name": "homepage", "path": "/", "expect_status": 200 }, { "name": "auth-page", "path": "/login", "expect_status": 200 }, { "name": "api-list-no-auth", "path": "/api/users", "method": "GET", "expect_status": 401 } ] ``` If both `extra_checks` and `/_smoke_tests.json` are present, both run — they're additive. ## Opt out await smoke_config_update({ project_id: '...', enabled: false }) Use for projects whose homepage is intentionally a 401 (private apps) or where the cron is a nuisance during a known outage window. You can still trigger `smoke_run` while disabled — the opt-out only stops the automated cron + deploy hooks. --- ## Tasks — Per-project ticketing (tasks) Track work, incidents, or any todo-shaped state per project. Tasks and their comments live in the project's own database (auto-created on first write) so they travel with db_export / db_restore. Free + unlimited. Every mutation publishes a realtime event automatically. ## From a deployed function ```js const t = await sw.tasks.create({ title: 'Investigate latency', priority: 'high', area: 'api', }) await sw.tasks.comment(t.id, 'spike started ~14:02 UTC') await sw.tasks.update(t.id, { status: 'in_progress', assignee: userId }) // Break work into sub-tasks. const sub = await sw.tasks.create({ title: 'Profile the slow endpoint', parent_id: t.id, area: 'api', }) const open = await sw.tasks.list({ status: 'open', limit: 100 }) const apiOnly = await sw.tasks.list({ area: 'api' }) const topLevel = await sw.tasks.list({ parent_id: 'null' }) // no parent const children = await sw.tasks.list({ parent_id: t.id }) const full = await sw.tasks.get(t.id) // includes comments[] await sw.tasks.delete(t.id) ``` ## Fields - `id` (`tsk_…`) - `title` (required), `description` - `status`: `backlog` | `open` | `in_progress` | `blocked` | `needs_review` | `done` | `archived`. `backlog` is for raw, untriaged ideas — devs filter with `status:'open'` so backlog doesn't surface in active queues; promote when ready to work. Use `blocked` when external action is required, `needs_review` when work is done but waiting on sign-off. - `priority`: `low` | `normal` | `high` | `urgent` - `assignee`, `reporter` (defaults to caller) - `labels`: string[] - `due_at`: ms epoch - `area`: free-form subsystem tag (e.g. `billing`, `auth`, `ui`) for filtering related tasks - `parent_id`: optional parent task ID — makes this a sub-task. A task cannot be its own parent. Deleting a parent does NOT cascade — detach or delete sub-tasks first if you want them gone. - `created_at`, `updated_at`, `completed_at` (auto-synced when transitioning to / from `done`) ## Notifications Every mutation publishes `task.created` / `task.updated` / `task.deleted` / `task.commented` on the project's `system:project` realtime channel — no config required. Optionally fan out to a webhook URL and / or email: ```js const { webhook_secret } = await sw.tasks.settings.update({ webhook_url: 'https://example.com/task-webhook', notify_email: 'alerts@yourcompany.com', }) // Store webhook_secret — it's returned exactly once. ``` Webhook signature header: `X-Somewhere-Signature: t=,v1=` — signed body is `${ms}.${rawBody}`. Same scheme as db_webhooks and inbox. Pass an empty string to clear a field. Clearing `webhook_url` also clears the secret. `sw.tasks.settings.get()` never returns the secret — only the first `settings.update` that mints one does. ## MCP tools - `tasks_create` / `tasks_list` (pass `task_id` to read one task with its comments) / `tasks_update` (also accepts `comment` to append a comment) / `tasks_delete` - `tasks_settings_get` / `tasks_settings_update` ## Default ordering `tasks_list` and `sw.tasks.list({})` sort active tasks ahead of `done` ones, then by priority (`urgent` → `high` → `normal` → `low`), then by `created_at DESC` within each priority. Pass an explicit filter (`status`, `priority`, `area`, `parent_id`) to narrow. There's no `order_by` field — if you need a different order, sort client-side. ## Closing a task: always include a resolution_note ```js await sw.tasks.update(id, { status: 'done', resolution_note: 'Root cause: stale CDN cache. Purged + added cache-bust query string. No customer impact.', }) ``` The note is persisted as a comment AND included in the resolution email + webhook payload so the reporter knows what was done. If you omit it on a task with zero prior comments, the API returns the same `{ ok: true }` response with an extra `hint` field nudging you to add one. The warning is non-blocking — bulk triage of stale tickets doesn't need a note on every row. ## Recommended practices - **Priority honestly.** Reserve `urgent` for things actively breaking customers. Default to `normal`. `high` is for "needs this sprint". - **Use `area` consistently per project.** Pick 4–8 tags (`billing`, `auth`, `api`, `ui`, `infra`) and reuse them. The dashboard can filter on it; agents can list per-area open work. - **Parent + sub-tasks for multi-step work.** Use `parent_id` when a task naturally splits. Don't create a parent for a one-liner. - **Don't delete; close as `done` or `archived`.** The audit trail matters. `delete` is for malformed test rows. --- ## sw.rateLimit — Rate limiting (sw.rateLimit) Throttle abusive callers without standing up Redis or a token-bucket library. One method, fixed-window counter, per-second granularity. ## sw.rateLimit.check(key, max, windowSeconds) Returns `{ allowed, remaining, reset, limit, window_seconds }`. On `allowed: false`, return 429 to the caller. `reset` is the epoch seconds when the current window rolls over. ## Per-IP login throttle ```js export async function POST(req, sw) { const ip = req.headers.get('cf-connecting-ip') || 'unknown' const rl = await sw.rateLimit.check(`login:${ip}`, 5, 60) if (!rl.allowed) { return Response.json( { error: 'too_many_attempts', retry_at: rl.reset }, { status: 429 } ) } // …handle login } ``` ## Per-user API throttle ```js const user = await sw.auth.fromRequest(req) const rl = await sw.rateLimit.check(`api:${user.id}`, 100, 60) if (!rl.allowed) return Response.json({ error: 'rate_limited' }, { status: 429 }) ``` ## Per-endpoint global cap ```js const rl = await sw.rateLimit.check('signup', 1000, 3600) // 1k signups/hour if (!rl.allowed) return Response.json({ error: 'paused' }, { status: 429 }) ``` ## Notes - Keys are scoped per project automatically — `login:1.2.3.4` in your app does not collide with the same key in someone else's app. - `max` is a positive integer up to 1,000,000. - `windowSeconds` is between 1 and 86,400 (24h). - Counters are best-effort fixed-window. A small under-count is possible at window boundaries on highly concurrent calls — fine for throttling, not appropriate for hard quotas. --- ## Plans & Pricing (billing) Pricing is DB-driven (the tier_plans table) and served live at /v1/pricing — always fetch that for the current numbers rather than hardcoding them. Every tier includes UNLIMITED projects, deploys, and bandwidth. Custom domains are free on every tier. Payments: a 0.5% platform fee applies on top of Stripe's standard processing fee (returned as `fee_percent`). AI: pay-as-you-go from a prepaid balance; the Free tier gets free models only (rate-limited), paid models draw from balance on every plan. Inbox: metered and billed per inbound email, never capped or time-expired. All current rates are at /v1/pricing. ## Tiers The ladder is Free → Builder → Pro → Scale → Enterprise. Every tier is the FULL platform (functions, database, auth, email, AI, payments, files, inbox, realtime, background jobs) — gated by CAPS, not features. Higher tiers raise the caps (files, database, email/day, realtime, upload size) and add advisory debugging, autonomous repair, and a stronger security review. The exact tier prices, the per-tier caps, and which capabilities unlock at each tier are served live from `/v1/pricing` — fetch that for current numbers rather than hardcoding them here. ## The "unlimited" caveat Advertised "unlimited" (e.g. database rows / auth users on higher tiers) means a high SAFETY cap, never literally uncapped — to protect shared infrastructure. Row/user counts are metered with warnings before any cap; the platform never silently blacks out a live app at a limit. ## Check your plan / upgrade Your plan is per-ACCOUNT (not per-project) — see it in Dashboard → Billing. Upgrade there, or via payments_checkout with the tier's Stripe Price ID from /v1/pricing. --- ## Platform Feedback — Bug? Doc gap? Tell us. (feedback) If the platform itself blocks you — an MCP tool doesn't behave like its description, an endpoint returns a surprising shape, the docs contradict the live behavior, a feature is missing — file a ticket. You don't have to be a senior debugger to file one; "I tried X, expected Y, got Z" is already useful. ## Submit ```ts const t = await platform_feedback({ message: "PUT /v1/fs binary upload returns 200 but reads back UTF-8-replaced. Repro: curl --data-binary @cat.jpg ...", project_id: "my-app", // optional context: { endpoint: "PUT /v1/fs", file_size: 82823 }, // optional }); // → { ticket_id: "pfb_a1b2c3d4e5f6", status: "open", created_at: 1715000000000 } ``` Our team is alerted immediately with your message and ticket id. They reply on the ticket — and **when they do, you get an email** at the address on your account. No polling required. ## Read back Use these if you want to check before the email lands: - `platform_feedback({ ticket_id })` — one ticket, full detail including the response. - `platform_feedback({ status?, limit? })` — every ticket you've filed (most recent first), with status and response inline (omit `message` and `ticket_id` to list). ## Shape returned by status / list ```json { "ticket_id": "pfb_a1b2c3d4e5f6", "status": "open" | "investigating" | "resolved", "message": "your original report", "response": "the team's reply (null until they answer)", "responder": "support@somewhere.tech", "created_at": 1715000000000, "resolved_at": null | 1715000000000 } ``` ## What "good" looks like A useful ticket gives the platform team three things in 4–5 lines: 1. **What you tried** — exact endpoint or MCP tool + the args. 2. **What you expected** — based on what doc or precedent. 3. **What happened** — the raw response or error code, not a paraphrase. Bad ticket: "files don't work, please fix". Good ticket: "POST /v1/fs with content-type application/octet-stream and binary body returns 200, but the readback replaces every byte ≥0x80 with EF BF BD. AGENT.md says binary is supported." ## When to use which - **Stuck on architecture** → `platform_advisor({ question })`. - **The platform itself is broken or unclear** → `platform_feedback`. - **You think the docs are wrong** → `platform_feedback`. The docs are derived from this file (`platform-help`) and `AGENT.md`. If it's wrong here, it's wrong everywhere. ## What the team does with it Open tickets surface in our team's dashboard, get triaged, and either get a fix shipped (which closes the ticket with a deployed-in note) or a workaround written into the response. The bug gets logged and the next agent that hits it sees the fix in the docs, not the same dead end. --- ## Payments — Stripe Connect (payments) The platform wires Stripe Connect for you. You get `payments_*` MCP tools and `sw.payments` inside deployed functions. End-customers pay through a Stripe Checkout Session and money flows to the developer's connected account, minus Stripe's standard processing fees and a 0.5% platform fee (each checkout response carries `fee_percent`). ## You DO NOT need to onboard to start building Test-mode checkouts work immediately. Call payments_checkout (or sw.payments.checkout) with env="dev" — when the developer hasn't onboarded yet, the session runs directly on the platform's own Stripe test account (no Connect destination, no application fee), so card 4242 4242 4242 4242 succeeds end-to-end. The response includes `is_stand_in: true` and `platform_fee_cents: 0` so callers know the fee won't apply on those sessions. Useful for development flows, integration tests, or validating checkout behavior before live onboarding. Once the developer calls payments_onboard, dev-mode checkouts switch to running against their own test connected account (still no live charges; same 4242 card). ```json // MCP — agent calling the platform payments_checkout({ project_id: "my-store", env: "dev", mode: "payment", line_items: '[{"amount": 4900, "currency": "usd", "name": "Premium plan"}]', success_url: "https://my-store.somewhere.tech/done", cancel_url: "https://my-store.somewhere.tech/cart" }) ``` ```js // Inside a deployed function — env is auto-detected from PROJECT_ENV. // Use env="prod" for live charges (default once payments_onboard is done). export default async function(req, sw) { const user = await sw.auth.fromRequest(req); if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 }); const { url } = await sw.payments.checkoutForUser(user.id, { plan: 'premium', mode: 'payment', line_items: [{ amount: 4900, currency: 'usd', name: 'Premium plan' }], success_url: 'https://my-store.somewhere.tech/done', cancel_url: 'https://my-store.somewhere.tech/cart' }) return Response.redirect(url, 303) } ``` For app-user entitlements, prefer checkoutForUser. The platform creates a checkout-intent row and sends only that intent id through Stripe metadata; webhooks derive app_user_id and plan from the platform database. Do not pass browser-controlled metadata.app_user_id or metadata.plan through a public handler. ## Onboarding for live charges Call payments_onboard when the developer is ready to take REAL money. It's per-account (NOT per-project) — one onboard covers every project the developer owns now or in the future. Returns a Stripe-hosted URL for KYC + bank info. ```json payments_onboard({ return_url: "https://my-store.somewhere.tech/payments/done", refresh_url: "https://my-store.somewhere.tech/payments/retry" }) ``` After the developer finishes onboarding, payments_status reports charges_enabled=true and you can call payments_checkout({ env: "prod" }). ## Collaborators Anyone with access to the project can run checkouts. Funds always go to the project OWNER's connected account. If the owner has not finished live onboarding and a collaborator calls payments_checkout({ env: "prod" }), the platform returns 412 with code OWNER_NOT_ONBOARDED and a message telling the caller the owner needs to onboard first. ## What's NOT done for you The user-facing pricing page, cart UI, and post-success receipts are your code. Stripe Connect handles money + tax + payouts. You handle "what does the customer click before they get to checkout". ## Webhook events The platform's own webhook (at /v1/payments/webhook) keeps the charges_enabled / payouts_enabled flags in sync. You don't need to register your own webhook for that. If you want to react to charge.succeeded / payment_intent.succeeded inside YOUR app, register a SECOND Stripe destination in the developer's connected account and point it at one of your deployed functions. ## Automatic plan tracking on app_users Want user.plan = "premium" to flip automatically when the customer pays — and to flip back when they cancel — without writing a single webhook handler? Use `checkoutForUser` from inside a deployed function. The platform mints a signed intent row server-side and forwards only its id through Stripe metadata, so a browser cannot spoof `app_user_id` or `plan`: const user = await sw.auth.fromRequest(req); if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 }); const { url } = await sw.payments.checkoutForUser(user.id, { plan: "premium", mode: "subscription", env: "prod", line_items: [{ price: "price_PREMIUM_MONTHLY", quantity: 1 }], success_url: "https://my-app.somewhere.tech/billing/success", cancel_url: "https://my-app.somewhere.tech/billing/cancel", customer_email: user.email, }); The platform's own Stripe Connect webhook will: - on checkout.session.completed → app_users.plan = "premium", plan_status = "active", and stamp stripe_customer_id + stripe_subscription_id. - on invoice.payment_failed → plan_status = "past_due" (plan stays the same so you can show "your premium will lapse" copy). - on customer.subscription.deleted → plan_status = "canceled". Read the current plan on the user object: const user = await sw.auth.fromRequest(req); if (user.plan_status !== "active") return Response.redirect("/billing"); if (user.plan === "premium") { /* unlock the feature */ } // Or directly: const { user } = await sw.auth.me(token); console.log(user.plan, user.plan_status); // → "premium", "active" (or "free", null when the user has never paid) Note: plan is always returned (defaults to "free" for users who have never checked out). plan_status is null for free users, and "active" | "past_due" | "canceled" once they've subscribed. ## Refunds, cancels, transactions, portal, events Five more endpoints round out the payments surface so you don't need to use the payment processor SDK or build a custom dashboard. ### Refund a charge ```js // Inside a deployed function. Pass payment_intent_id from the // checkout session (sw.payments.checkout returns it indirectly via // the session object; you typically store it on your order row). const refund = await sw.payments.refund({ payment_intent_id: 'pi_3PqXYZ...', // amount: 1000, // omit for full refund (in cents) // reason: 'requested_by_customer' }); // → { refund_id, status, amount, currency, charge_id, ... } ``` Stripe's processing fees are handled by Stripe's standard refund policy. ### Cancel a subscription ```js await sw.payments.cancelSubscription({ subscription_id: 'sub_...' }); // Default: cancel_at_period_end=true — customer keeps access until period end. await sw.payments.cancelSubscription({ subscription_id: 'sub_...', immediately: true }); // Stops billing now and ends access. ``` `app_users.plan_status` is updated automatically by the webhook on `customer.subscription.deleted` — no extra wiring required. ### List recent transactions ```js const { transactions, next_cursor } = await sw.payments.transactions({ limit: 20 }); // → transactions: [{ id, amount, currency, status, paid, refunded, // created, customer_id, payment_intent_id, ... }] // Paginate with: sw.payments.transactions({ limit: 20, starting_after: next_cursor }) ``` ### Open the Stripe Billing Portal for an end-customer The portal lets customers update their card, switch plans, view invoices, and cancel themselves — no support ticket required. ```js const { url } = await sw.payments.portal({ customer_id: user.stripe_customer_id, return_url: 'https://my-app.somewhere.tech/account/billing', }); return Response.redirect(url, 303); ``` (Stripe auto-creates a default portal configuration on first use. Advanced settings — what plans are switchable, custom branding — need a one-time click in the connected account's Stripe dashboard.) ### Read the webhook event ledger Every Stripe event the platform's webhook receives is persisted in a ledger you can read back. The handler is idempotent — Stripe redeliveries (which happen on transport failures) appear exactly once. Use this to drive an in-product activity feed or to verify delivery during integration. ```js const { events, next_cursor } = await sw.payments.events({ limit: 50, // type: 'checkout.session.completed', // optional filter // before: 1715000000000, // ms-epoch cursor }); // → events: [{ id, type, mode, account_id, project_id, // amount_cents, currency, livemode, received_at }] ``` ### One-time vs subscription `payments_checkout` accepts `mode: 'payment'` (one-time charge) or `mode: 'subscription'` (recurring billing). Same endpoint, same automatic plan-tracking metadata. Default is `'payment'`. --- ## Platform-owned responsibilities (guarantees) This section enumerates behavior the platform performs outside application code. Application code should depend on these behaviors instead of duplicating them. --- ## Security responsibilities ### Auto-scoped database queries - `sw.db.query(sql, params, { user })` injects the configured owner predicate for user-scoped tables. - Scoped INSERT operations set the owner field server-side. - Scoped UPDATE and DELETE operations add the owner predicate server-side. - Unscoped access to a declared scoped table throws `SCOPE_VIOLATION` unless `{ unscoped: true }` is explicitly supplied by server-side code. - The deploy scanner flags authenticated handlers that perform raw `sw.db.query(...)` without a user scope when the pattern is detected. ### Private file enforcement - Uploaded files are private unless explicitly made public. - File serving checks visibility before returning bytes. - Signed upload flows default to private visibility. - Project-deployed static assets default to public visibility. ### SSRF protection on platform-side fetches - User-controlled URL fetches used by platform primitives pass through `safeFetch`. - The fetch layer resolves DNS, blocks private addresses, link-local addresses, and metadata-service addresses, validates redirects, enforces timeout, and enforces byte caps. - Covered surfaces include webhooks, AI transcription by URL, render, scrape, and other platform-owned URL fetch paths. ### Per-project JWT signing keys - Each project derives its own JWT signing key from platform secret material and the project identifier. - An app-user token for one project is not accepted for another project. ### Atomic token consumption - Password reset tokens, magic-link tokens, refresh tokens, and MFA challenges are consumed atomically. - A consumed token returns invalid on later use, including concurrent use attempts. ### Compile-time URL and resource limits - Deploy compilation blocks imports from attacker-controlled URLs. - Deploy limits include source-file count, source-byte size, and binary-size caps according to the deploy reference. ### App-user lockdown - App-user JWTs are accepted only on routes that explicitly allow app-user authority. - App-user JWTs cannot reach platform-admin endpoints, project lifecycle operations, billing, domains, environment variables, key management, or table enumeration. ### Deploy security review - `security_review` reads deployed source and returns findings against platform-defined risk categories. - The deterministic guardrail scanner runs during compile for high-confidence patterns and surfaces warnings in the deploy response. ### Payment metadata validation - Checkout validates line items server-side against the project's configured price catalog. - Client-provided arbitrary amount or currency values are not trusted for platform-tracked checkout. - Return URLs are pinned to the project domain rules documented in the payments reference. --- ## Reliability responsibilities ### Platform-level CORS - `/api/*` OPTIONS preflights return 204 with platform-managed CORS headers. - Function responses receive the documented CORS response headers. ### Blank-page deploy gate - A headless browser captures the live homepage after promote. - A screenshot below the documented byte threshold triggers `DEPLOY_BLANK_PAGE`; the deploy is flagged and an alert is recorded. ### Deploy failure alerting - Deploy, promote, patch, blank-page, and health-check failures are detected with project, version, and cause metadata. ### Automatic health checks - Deployed projects receive scheduled smoke tests according to tier. - A pass-to-fail transition is surfaced through platform state and alerts. ### npm import resolution - Deployed functions can import npm packages. - Versions declared in `package.json` are used as pins. - Runtime built-ins are passed through as externals. - There is no application-level `npm install` step in the deploy path. ### Dev/prod serving slots and rollback - Project deploys are versioned. - Rollback restores a prior version through platform deploy history. - `expected_version` allows writers to refuse overwrite when another deploy landed first. --- ## Infrastructure responsibilities ### Zero-configuration compilation - Raw `.jsx`, `.tsx`, `.ts`, `.html`, `.css`, function files, and assets are accepted as deploy input. - The platform compiles JSX/TSX and TypeScript at deploy time. - Bundled output can be rejected with `BUNDLED_DEPLOY_REJECTED`. ### One runtime binding - Deployed functions receive `sw` with database, auth, files, email, payments, AI, jobs, queue, cron, logs, environment variables, and other primitives bound to the project. - Runtime functions do not need developer API keys to call platform primitives. ### Usage accounting - Platform usage is visible through plan and usage surfaces. - Current tier limits are obtained from the pricing endpoint rather than hardcoded from this document. ### Custom domain provisioning - Domain attachment writes the required routing state, certificate state, and project binding state. - Nameserver delegation is the recommended path when the platform should manage apex, `www`, subdomains, and optional inbox routing. --- ## Post-deploy intelligence responsibilities ### LLM security review - Available as a platform tool and REST endpoint according to plan. - Produces structured findings against the documented risk categories. ### Live-site screenshots - Desktop and mobile screenshots are captured per version. - Screenshots are available through project screenshot surfaces. ### Architecture diagram - The platform produces a Mermaid architecture diagram from deployed source per version. ### Application description - The platform produces a short application description from deployed source per version. --- ## Getting Started — Build path (getting-started) Use the following sequence for a new project: install/authenticate, create project, configure auth, create tables, implement functions, implement frontend, deploy. ## Step 0 — Install the CLI and log in ```bash npm i -g @somewhere-tech/cli somewhere auth login ``` A browser window opens, you approve, the session lands in `~/.somewhere/config.json`. CLI, MCP bridge, and `somewhere deploy` all read it. Browser device authorization flow. Do **not** ask the user for an API key. Do **not** suggest `SMT_API_KEY=...`. The `smt_` key still exists for CI/CD — never for a human setup flow. ## Step 1 — Create the project ```bash mkdir my-app && cd my-app somewhere init --name my-app ``` `somewhere init` creates the project on the platform, claims the subdomain, writes `.somewhere.json` so deploys know which project to target, and wires Claude Code's MCP config in one shot. From the agent side, MCP `project_create` does the same backend call if you'd rather stay inside the chat. ## Step 2 — Wire Google auth (always do this first) Two function files. That's it. ```typescript // api/auth/google.ts — sends the user to Google export default async function (req, sw) { const url = sw.auth.googleUrl({ redirect_uri: \\`https://\\${sw.subdomain}.somewhere.tech/api/auth/google-callback\\` }) return Response.redirect(url, 302) } ``` ```typescript // api/auth/google-callback.ts — exchanges the code for a session token export default async function (req, sw) { const code = new URL(req.url).searchParams.get('code') if (!code) return Response.json({ error: 'missing code' }, { status: 400 }) const { token } = await sw.auth.googleExchange({ code }) return new Response(null, { status: 302, headers: { 'Set-Cookie': \\`token=\\${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600\\`, Location: '/', }, }) } ``` ## Step 3 — Wire email auth ```typescript // api/auth/signup.ts export default async function (req, sw) { const { email, password } = await req.json() const result = await sw.auth.signup({ email, password }) return Response.json(result) } // api/auth/login.ts export default async function (req, sw) { const { email, password } = await req.json() const result = await sw.auth.login({ email, password }) return Response.json(result) } // api/auth/me.ts export default async function (req, sw) { const user = await sw.auth.fromRequest(req) if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 }) return Response.json(user) } ``` `sw.auth.fromRequest(req)` reads the cookie or Authorization header and returns the validated user — or null. **Never** parse cookies manually inside a function. ## Step 4 — Create your database tables (developer-side, not in a function) `sw.db.migrate` was removed from the function runtime (2026-05-21) — a deployed handler can't run DDL. Apply your schema once, up front, with the `db_migrate` MCP tool (or the CLI / dashboard). The block below is the call to make, not code to drop into a function: ```typescript // developer-side — db_migrate MCP tool (NOT inside a function): db_migrate({ project_id, sql: ` CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, title TEXT NOT NULL, body TEXT, created_at INTEGER DEFAULT (unixepoch()) ); ` }) ``` Or call `db_migrate_to` from MCP with a JSON schema. Both work; the JSON form is nicer if you want the platform to compute the diff. ## Step 5 — Build your product logic API functions in `api/`. Each file is a route. Use `sw.db`, `sw.ai`, `sw.fs`, `sw.email` — not `fetch('https://api.somewhere.tech/...')`. ```typescript // api/posts.ts export default async function (req, sw) { const user = await sw.auth.fromRequest(req) if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 }) if (req.method === 'GET') { const { data } = await sw.db.query('SELECT * FROM posts WHERE user_id = ?', [user.id]) return Response.json(data) } if (req.method === 'POST') { const { title, body } = await req.json() await sw.db.query('INSERT INTO posts (user_id, title, body) VALUES (?, ?, ?)', [user.id, title, body]) return Response.json({ ok: true }) } } ``` ## Step 6 — Build the frontend Plain `index.html` (or React, or whatever) that calls your `api/*` endpoints with `fetch`. The cookie set in Step 2 is sent automatically on same-origin requests. ## Step 7 — Deploy ```bash somewhere deploy ``` The CLI bundles + uploads in one step. The live URL prints when it's live (~3s). Every deploy is live — no separate promote step. ## DO NOT - ❌ Build a `messages` or `conversations` table for chat history. Use `conversation_id` on `sw.ai.chat` — the platform persists + truncates for you. - ❌ Write `getCookie()` or token parsing. Use `sw.auth.fromRequest(req)`. - ❌ Write anon-session management by hand. Use `sw.auth.anonSession(req)` + `sw.auth.migrateAnon({ anonId, userId })` on signup. - ❌ Write retry / timeout / rate-limit loops around `sw.*` calls. The platform handles upstream retries already. - ❌ Deploy via MCP `project_deploy` for a single-file edit. Use the CLI's `somewhere deploy` for full deploys, or MCP `project_patch` for a single-file patch. - ❌ Call `https://api.somewhere.tech/v1/...` from inside a deployed function. `sw.*` is the same surface through the runtime binding and does not require an API key in application code. - ❌ Put environment variables for `sw.*` services (database, AI, email, etc). `sw.*` is already authenticated — env vars are for YOUR external services (Stripe, Twilio, etc). ## Default model recommendation Production default: **`claude-sonnet-4-6`**. ```typescript await sw.ai.chat({ provider: 'anthropic', model: 'claude-sonnet-4-6', messages: [...] }) ``` The free models on `provider: 'workers-ai'` (`@cf/meta/llama-4-scout-...`) are useful for development, smoke tests, or scripts where quality doesn't matter. For user-visible AI responses, use a paid production model rather than a development-only model. ## Available platform_help topics Call platform_help({ topic: "" }) for any of: Onboarding setup — slower step-by-step setup flow architecture-patterns — chat app, REST API + auth, file uploads, jobs common-mistakes — real failure modes the platform has seen troubleshooting — error message → fix lookup Inside-function references (sw.* services your code uses) sw.db, sw.fs, sw.ai, sw.email, sw.env, sw.jobs, sw.queue, sw.logs, sw.auth, sw.rateLimit, sw.push, sw.image Developer-side surfaces projects, deploy, dev-prod, domains, search, render, video, analytics, inbox, calls Platform features functions, realtime, cron, billing --- ## Setup — Install the CLI and connect MCP (setup) Setup path for CLI and MCP access. ## 1. Install the CLI ```bash npm i -g @somewhere-tech/cli ``` The package is `@somewhere-tech/cli`. The binary is `somewhere`. Requires Node 18+. The CLI works on macOS, Linux, and Windows (via WSL or PowerShell). ## 2. Log in (browser-based, no key paste) ```bash somewhere auth login ``` A browser window opens, you approve the device, the session lands in `~/.somewhere/config.json`. Browser device authorization flow. After this you can run `somewhere deploy`, `somewhere logs`, and the MCP bridge — all read the same config file. Do **not** paste an `smt_` key into a config file. The smt_ developer key still exists for CI / server-to-server, but for a human setup flow always use `auth login`. ## 3. Connect the MCP to Claude Code The Somewhere-Tech MCP bridge ships as a Claude Code plugin. Four steps: ```text /plugin marketplace add Somewhere-Tech/claude-code-plugin /plugin install somewhere-tech@somewhere-tech-claude-code-plugin # (reload Claude Code) somewhere auth login # if you haven't yet ``` After reload, every `mcp__somewhere__*` tool is available — `db_query`, `fs_write`, `project_deploy`, `platform_advisor`, etc. The bridge forwards the CLI's session to `mcp.somewhere.tech` so you don't paste a key. ## 4. Deploy something React and function example — the platform compiles the JSX and the server-side TypeScript automatically at deploy time. No build step. ```bash mkdir hello && cd hello ``` `index.html` — entry point: ```html hello
``` `src/main.tsx` — React app (raw TSX, deployed as-is): ```tsx import { createRoot } from 'react-dom/client'; import { useEffect, useState } from 'react'; function App() { const [msg, setMsg] = useState('loading...'); useEffect(() => { fetch('/api/hello').then(r => r.json()).then(d => setMsg(d.message)); }, []); return

{msg}

; } createRoot(document.getElementById('root')!).render(); ``` `api/hello.ts` — server function (raw TS, deployed as-is): ```ts export default async function(req, sw) { return Response.json({ message: 'hello from a function' }); } ``` Now deploy: ```bash somewhere deploy ``` The CLI uploads the source files. The platform's compiler bundles `src/main.tsx` into a hashed JS chunk under `/_compiled/`, rewrites `index.html` to load it, and compiles `api/hello.ts` into a function the runtime can execute. By default the URL serves prod. To preview a dev build first, add `?env=dev` to the URL. (For just-a-static-page, you can ship a single `index.html` with plain HTML — the JSX/TSX compiler only runs on files that need it.) ## Common follow-up commands ```bash somewhere logs # tail recent function logs somewhere deploy # redeploy current directory somewhere mcp stdio # raw stdio MCP bridge (for non-plugin clients) somewhere auth status # show current login ``` For everything else (custom domains, project rename, env vars, billing) go through the dashboard at https://somewhere.tech or call the matching MCP tool from your editor. ## Ephemeral environments (Claude Code Web, sandboxes, containers) If the user is in an environment without a persistent home directory (Claude Code Web, a fresh CI container, a sandboxed runner), the browser-based `somewhere auth login` won't survive the session — but the MCP connection already has an authenticated session. Pair the CLI to that session in one step: ```text 1. npm i -g @somewhere-tech/cli # ~5 seconds 2. Call MCP tool auth_cli_pair # returns { key, expires_at } 3. somewhere auth set # 24h-TTL token 4. somewhere whoami # confirm identity 5. somewhere deploy # ready to go ``` `auth_cli_pair` mints a short-lived `smt_` key (kind='cli_pair', 24h TTL) scoped to the same user as the MCP session. The agent writes the token to the CLI config; the CLI works for the rest of the session. The key auto-expires; revoke early with `DELETE /v1/keys/` if you need to. Pure-MCP path (Claude.ai connector, no shell at all): skip the CLI entirely. `project_deploy` does the same write `somewhere deploy` would do — it just costs more tokens because the files round-trip as JSON. Reads (`fs_read`, `db_query`, `log`) are identical either way. --- ## Common Mistakes — Things real users have hit (common-mistakes) **STOP.** Before writing any code, check if the platform already handles it. Eight rules cover most of the wasted-time tickets: - ❌ Don't build a `messages` or `conversations` table for chat history → use `conversation_id` on `sw.ai.chat()`. The platform loads, persists, and truncates history for you. - ❌ Don't write `getCookie()` / token-parsing helpers → `sw.auth.fromRequest(req)` reads cookie + Bearer header and returns the validated user (or null) in one call. - ❌ Don't write anon-session management → `sw.auth.anonSession(req)` for the cookie + uuid, `sw.auth.migrateAnon({ anonId, userId })` to hand the rows over on signup. - ❌ Don't hand-roll Stripe webhook handling — see the `payments` topic for the supported flow. - ❌ Don't deploy a single-file change via MCP `project_deploy` — that replaces every file in the project. Use `project_patch` for a single file or the `somewhere deploy` CLI for a full deploy. - ❌ Don't write retry / timeout / rate-limit loops around `sw.*` — the platform retries upstream calls already; nested retries make things worse. - ❌ Don't call `https://api.somewhere.tech/v1/...` from inside a deployed function → use `sw.*` (same surface, no network hop, no API key juggling). - ❌ Don't set env vars for platform services (`SOMEWHERE_API_KEY`, database URLs, etc.) → `sw.*` is already authenticated. Env vars are for YOUR external services (e.g. a payment provider, an SMS API, your own AI key, etc). The rest of this topic lists observed failure modes and corrections. ## 1. Calling api.somewhere.tech from inside a deployed function **Wrong:** ```typescript // inside a deployed function const res = await fetch('https://api.somewhere.tech/v1/db/query', { headers: { Authorization: 'Bearer smt_...' }, ... }) ``` **Right:** ```typescript const res = await sw.db.query('SELECT * FROM users') ``` Inside a deployed function you have direct bindings: `sw.db`, `sw.fs`, `sw.ai`, `sw.email`, `sw.auth`, `sw.env`, `sw.jobs`, `sw.queue`, `sw.logs`. Each one talks to the platform without a network hop and without auth juggling. The REST surface (`api.somewhere.tech`) is for code running OUTSIDE the platform — your laptop, a third-party server, a webhook handler somewhere else. ## 2. Using project_deploy when you meant project_patch `project_deploy` **replaces** every file in the project. If you pass `{ files: { "index.html": "..." } }`, every other file is deleted. For a single-file edit, use `project_patch` — one file per call. Two modes: `{ path, content }` rewrites the file, or `{ path, find, replace }` does a server-side substring swap (~200 bytes on the wire). The platform auto-routes the path to static or function. For multi-file changes, call `project_patch` once per file (the platform preserves everything you don't name). ## 3. Putting tokens or JWTs in URLs Don't do this: ```text https://my-app.somewhere.tech/dashboard?token=eyJhbGc... ``` Tokens in query strings end up in: - Browser history - Server access logs - HTTP referrer headers when the page links out - Anyone watching over your shoulder Use cookies (httpOnly, secure, sameSite) or the `Authorization: Bearer` header. The auth flows the platform ships with already do this — only break the pattern if you have a very specific reason. ## 4. Putting smt_ keys in browser-accessible code The `smt_` developer key is admin. It can read every project, deploy code, change env vars. If it leaks (committed to GitHub, embedded in JavaScript bundle, dropped in browser localStorage) someone can take over the account. Use the smt_ key only: - Server-side, in env vars - In CI/CD secrets - In your local terminal For browser code, mint app-user JWTs through `sw.auth.signup` / `sw.auth.login`. JWTs are scoped to one user, one project — leak blast radius is one account. ## 5. Reasoning models with too-low max_tokens Reasoning models (the ones `ai_catalog` marks as reasoning) think first, then answer. The `` block consumes tokens. If `max_tokens` is too small, the model burns its budget on reasoning and returns an empty response. **Wrong:** `max_tokens: 500` for a reasoning model. **Right:** `max_tokens: 4000` minimum, `8000+` is safer. For non-reasoning models, 500 is fine. ## 6. sw.fs.write camelCase vs snake_case The `sw.fs.write` binding accepts both `contentType` (camelCase) and `content_type` (snake_case) for the MIME type. Other fields are camelCase only. Pick one style per project for consistency, but the platform tolerates either on this option. ```typescript await sw.fs.write('/uploads/avatar.png', bytes, { contentType: 'image/png' // or content_type — both work }) ``` ## 7. "My deploy didn't update the live site" Every `project_deploy` and `project_patch` writes to the live serving slot. `https://{subdomain}.somewhere.tech` serves your latest deploy immediately — no promote step. If the live site looks stale, it's almost always one of: - Browser or CDN cache. Hard-reload (Cmd-Shift-R), or check with `curl -sI https://{subdomain}.somewhere.tech`. - The deploy hit a different project than you think (wrong `project_id`). - Function changes can take 2-4 seconds to propagate; static changes are ~1s. `?env=dev` is preserved as an internal opt-in for testing the same bytes from the dev slot — useful when you want to bypass any intermediate cache layer. ## 8. Auth emails not arriving The platform sends auth emails (signup confirmation, password reset) from `noreply@somewhere.tech` by default. That domain is not on your project — it's the platform's. Some email providers flag it as suspicious because the sending domain doesn't match your app's domain. Fix: add a verified sender domain in dashboard Settings. Once verified, auth emails go out from `noreply@yourdomain.com` and deliverability improves. ## 9. Forgetting to redeploy after rotating an env var `sw.env.MY_KEY` reads the value baked into the running function bundle. When you rotate an env var via the dashboard or `POST /v1/projects/:id/env`, **the running function still sees the old value until the next deploy**. Run `project_deploy` (or `project_patch` with no changes — empty patch is allowed) to refresh. --- ## Architecture Patterns — How to wire common app shapes (architecture-patterns) Each pattern is a working sketch you can lift and adapt. ## Two ways to use the platform There are two distinct entry points. Pick based on where your code runs. **Outside the platform** (your laptop, CI, a different host): - Auth: `Authorization: Bearer smt_...` developer key (or app-user JWT for end-user calls) - Surface: REST at `https://api.somewhere.tech/v1/*` and MCP at `https://mcp.somewhere.tech/mcp` - Use case: deploy scripts, admin tooling, agent calls, webhooks from third parties **Inside a deployed function** (code running on the platform): - Auth: none — the binding is pre-scoped to your project - Surface: `sw.db`, `sw.fs`, `sw.ai`, `sw.email`, `sw.auth`, `sw.env`, `sw.jobs`, `sw.queue`, `sw.logs` - Use case: API endpoints, server-side logic, AI proxies, file uploads, scheduled work When you write a function, default to `sw.*`. Only fall back to the REST surface when you have a specific reason (calling another project, calling from outside). ## Pattern: Chat app with auth + history Database: ```text users(id, email, password_hash, ...) -- created by sw.auth.signup _conversations(id, project_id, ...) -- platform-managed _conversation_messages(...) -- platform-managed ``` Function (`api/chat.ts`): ```typescript export default async function (req, sw) { const { user } = await sw.auth.me(req.headers.get('Authorization')?.slice(7)) if (!user) return new Response('Unauthorized', { status: 401 }) const { conversation_id, message } = await req.json() const result = await sw.ai.chat({ provider: 'anthropic', model: 'claude-sonnet-4-6', messages: [{ role: 'user', content: message }], conversation_id // platform persists + truncates history }) return Response.json({ conversation_id: result.conversation_id, reply: result.content[0].text }) } ``` Conversation history is stored on the platform — you don't manage it. Pass the same `conversation_id` on subsequent calls and the platform loads + truncates history automatically. ## Pattern: REST API with end-user auth Function (`api/posts.ts`): ```typescript export default async function (req, sw) { const jwt = req.headers.get('Authorization')?.slice(7) const { user } = await sw.auth.me(jwt) if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 }) if (req.method === 'GET') { const { data } = await sw.db.query( 'SELECT * FROM posts WHERE author_id = ? ORDER BY created_at DESC', [user.id] ) return Response.json(data) } if (req.method === 'POST') { const { title, body } = await req.json() const { data } = await sw.db.query( 'INSERT INTO posts (author_id, title, body) VALUES (?, ?, ?) RETURNING *', [user.id, title, body] ) return Response.json(data[0], { status: 201 }) } } ``` Auth is a single `sw.auth.me(jwt)` call. The platform validates the token, returns the user, and you scope queries by `user.id`. ## Pattern: File upload with public URL Function (`api/upload.ts`): ```typescript export default async function (req, sw) { const { user } = await sw.auth.me(req.headers.get('Authorization')?.slice(7)) if (!user) return new Response('Unauthorized', { status: 401 }) const formData = await req.formData() const file = formData.get('file') as File const buf = await file.arrayBuffer() const path = `/uploads/${user.id}/${crypto.randomUUID()}-${file.name}` await sw.fs.write(path, new Uint8Array(buf), { contentType: file.type }) // Public URL — anyone with the URL can read. const url = `https://${sw.env.PROJECT_SUBDOMAIN}.somewhere.tech/storage${path}` return Response.json({ url }) } ``` Files written under `/uploads/` are served on the project's domain at `/storage/`. For private files, use a separate path prefix and gate downloads through a function. ## Pattern: Background work Inline jobs (return immediately, work later): ```typescript // API endpoint export default async function (req, sw) { const { email } = await req.json() await sw.jobs.create({ handler: 'jobs/send-welcome', payload: { email } }) return Response.json({ queued: true }) } ``` Handler (`jobs/send-welcome.ts`): ```typescript export default async function ({ payload }, sw) { await sw.email.send({ to: payload.email, subject: 'Welcome', html: '

Hi!

' }) } ``` For fire-and-forget side effects use `sw.queue.push({ handler, payload })` — no return value, no status to inspect. For scheduled work use `cron_create` to point a cron expression at a function path. ## Pattern: External API proxy (hide your key) ```typescript // api/openai-proxy.ts — keeps OPENAI_API_KEY off the client export default async function (req, sw) { const { user } = await sw.auth.me(req.headers.get('Authorization')?.slice(7)) if (!user) return new Response('Unauthorized', { status: 401 }) const body = await req.json() const r = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${sw.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) return new Response(r.body, { status: r.status, headers: { 'Content-Type': 'application/json' } }) } ``` Set `OPENAI_API_KEY` as an env var on the project. The browser never sees it. Same pattern for Stripe, Twilio, anything. For built-in models (chat, image generation, TTS, embeddings, etc.) prefer `sw.ai.*` — billing is metered through the platform and you don't manage upstream keys at all. --- ## Troubleshooting — Common errors and what to do (troubleshooting) Pick the row that matches your error and apply the fix. ## "FUNCTION_NOT_FOUND" or 404 on a /api/* route The function file isn't deployed, or the route doesn't match. Check: 1. Did you actually deploy the function? `deploy_status({ project_id })` lists deployed functions. 2. Does the file path match the URL? `api/users/[id].ts` serves `/api/users/123`. `api/users.ts` serves `/api/users`. 3. Was the deploy actually successful? Re-run `project_deploy` and check the response — every deploy goes live; if the route still 404s, the function isn't in the bundle. ## "API key is invalid" inside sw.* calls The function bundle's runtime keys are stale. This happens after the platform rotates internal keys, or after a billing-state change that re-issued credentials. Fix: redeploy the project with any small touch, e.g. `project_patch({ project_id, path: "index.html", find: " ", replace: " " })` (a no-op find/replace bumps the version + reissues runtime keys). Re-running `project_deploy` works too but replaces all files, so `project_patch` is safer. ## "SPENDING_CAP_REACHED" The project hit its monthly AI spending cap. By default each project has a cap to prevent runaway costs. Fix: dashboard → Settings → AI → raise the cap. Or for testing, set the cap to a high value and leave it. Admin accounts bypass this gate automatically. The cap is per-project, not per-account. ## Auth emails (signup, password reset) not arriving Two common causes: **Sender not verified** — The platform sends from `noreply@somewhere.tech` by default. Some inboxes flag this as suspicious. Add a verified sender domain in dashboard Settings → Email, then auth emails come from your own domain. **Email not configured** — Email send goes through the platform email service. If the project's account doesn't have email sending wired up, sends fail silently (logged, not surfaced). Check dashboard Logs for the project to see the actual error. ## "Site not loading after deploy" Every `project_deploy` and `project_patch` writes to the live serving slot — there is no separate promote step. If the live site still shows the old version, it's almost always cache: 1. **Browser cache.** Hard reload (Cmd-Shift-R) or open the URL in a private window. 2. **Edge cache.** `curl -sI https://{subdomain}.somewhere.tech/` shows the response headers — `cache-control` tells you whether it was cached. 3. **Wrong project.** Confirm the `project_id` matches the subdomain you're hitting. If a deploy genuinely failed silently, `deploy_status({ project_id })` returns the most recent timestamp — older than your last deploy means the deploy errored. ## "MIGRATION_BLOCKED" on db_migrate_to The schema diff includes a destructive change (column drop, table drop, type change that loses data). The platform refuses by default to prevent silent data loss. Fix: review the diff, then re-run with `{ allow_destructive: true }` once you've confirmed the data is expendable. Always back up first with `db_query("SELECT * FROM ")` if in doubt. ## "DOMAIN_NOT_VERIFIED" on a custom domain The TXT record hasn't propagated, or doesn't match what the platform expects. Fix: 1. `domain_verify({ domain })` returns the exact TXT name + value you should set. 2. Wait 60-300 seconds after setting DNS for propagation. 3. Use `dig +short TXT _somewhere-challenge.` from terminal to confirm DNS-side before re-running verify. If the value is correct but verify still fails, the most common cause is multiple TXT records on the same name — the older one shadows the new one. Delete the stale record. ## "API_KEY is invalid" from sw.ai If you set a per-project AI provider key (e.g., your own provider key for higher rate limits), the runtime injects it into the function bundle. After rotating that key, you need to redeploy. Same fix as "API key is invalid" above. If you didn't set a custom key, you're using the platform-managed pool — `API_KEY is invalid` from `sw.ai` means the platform's upstream credential rotated and your bundle is stale. Redeploy. ## Deploy succeeds but old code still serves Browser cache, CDN cache, or you're on the wrong environment. In that order, try: 1. Hard reload (Cmd-Shift-R / Ctrl-F5). 2. Open the URL in a private window — bypasses local cache. 3. Check `deploy_status({ project_id })` to see the timestamp of the most recent deploy. If it's older than your last `project_deploy` call, the deploy actually failed and you missed the error. ## Hosted workspace ("/workspace") shows "Disconnected code=4001" The workspace machine is on an outdated agent version. The platform restart-loops it until either the image gets fixed or the machine is replaced. Fix is on our side. If you hit this, post in the support channel with your project_id and account email — it's not user-fixable. ## "Origin not allowed" when calling from a custom subdomain The project's CORS allowlist doesn't include the origin you're calling from. By default the platform allows the project's own `*.somewhere.tech` URL plus any verified custom domains for the project. Fix: dashboard → Settings → CORS, add the origin. Or if you're calling server-to-server, use the smt_ key — CORS doesn't apply to non-browser callers. ## End-users see "Something went wrong" on my live site When the deployed function throws or crashes during a page load, the platform serves a branded fallback page so visitors never see a raw infrastructure error or JSON blob. It looks like: > # Something went wrong > The app ran into a problem. Try again in a moment. Three things to know: 1. **The crash is in your function — read the stack trace.** Every uncaught exception is captured to your project's logs with the full stack: ``` log({ project_id, level: 'error', limit: 20 }) ``` Or open the dashboard → project → Logs tab and filter level=error. Each row has `data.stack`, `data.path`, `data.method`, and `data.request_id` (the `cf-ray` you can correlate with deeper logs if you contact support). 2. **The fallback only triggers for page loads.** `/api/*` requests still return JSON to JS callers — your client error handling stays the same. The branded page only appears when a *page* request itself fails. 3. **You can customize the page.** Upload a `_error.html` file (or `_error/index.html`) in your project. If present, the platform serves that on any unhandled error instead of the default. Plain static HTML — no templating, no function dispatch needed. ## How to read your error logs Every error on your project lands in `app_logs` and is reachable three ways: - **MCP**: `log({ project_id, level: 'error', limit: 50 })` returns the most recent error rows in JSON. Add `since: '1h'` or a unix-ms timestamp to scope the window. Combine with `q:` for a free-text search across message + data. - **Dashboard**: project → Logs tab. Filter by level, source (function | server | client | system), or search. - **From inside a function**: `sw.logs.error(message, data)` writes a row with source='server'. Use this for known-bad branches your code handles deliberately. What you'll see for an uncaught function exception: ```json { "level": "error", "source": "server", "message": "FUNCTION_ERROR: Cannot read properties of undefined (reading 'id')", "data": { "code": "FUNCTION_ERROR", "stack": "TypeError: Cannot read properties of undefined...\\n at handler (api/users.ts:14:18)\\n ...", "request_id": "8f3c1d2a4b5e6789-LHR", "method": "POST", "path": "/api/users", "route": "/api/users", "slot": "prod" }, "created_at": 1715551234567 } ``` The `stack` field is the single most useful field for debugging — it tells you the exact file + line in your code where the throw originated. Page-dispatch errors (worker couldn't start, function timed out, memory limit hit) show up here too with `code: "FUNCTION_DISPATCH_FAILED"`. Note: error capture is best-effort and runs via `ctx.waitUntil` — it never blocks the visitor's response, and the underlying request returns the branded error page either way. ```bash # Quick custom error page (write via fs_write or include in next deploy):

my-app is down

back in a sec

``` The default page is the right call most of the time — only override when your app's design needs a matching aesthetic. ## All API errors include a hint field Every JSON error from the platform now ships with a `hint` field that names the most relevant `platform_help` topic. Read the hint instead of guessing what to call next: ```json { "ok": false, "error": "VERSION_CONFLICT", "message": "Project version is 14, you sent 13.", "hint": "Try platform_help({ topic: 'deploy' }) for usage details..." } ``` If a hint looks misleading, send `platform_feedback({ message })` — you'll get a ticket_id you can check with `platform_feedback({ ticket_id })`. --- ## sw.web — Read pages from the public web (sw.web) Available inside any deployed function via the sw argument (ctx still works as an alias). sw.web fetches public web pages and returns their content as clean markdown. Use it when an agent or feature needs to ingest content from a URL it doesn't host — docs, blog posts, marketing pages, articles. The platform proxies the call so your function never holds the upstream key and never has to fight CORS. ## sw.web.scrape(url, opts?) Fetch a URL and return its content as markdown by default. Strips nav/footer/sidebar so you get the main article body. const page = await sw.web.scrape('https://docs.somewhere.tech/getting-started') // page = { // url: '...', title: 'Getting started', description: '...', // language: 'en', status_code: 200, // markdown: '# Getting started\\n...' // } opts: formats? — array of: markdown | html | rawHtml | links | screenshot Default: ['markdown']. only_main? — strip nav/footer/sidebar (default true). Set false to keep the full DOM. wait_for? — ms to wait for the page to settle (max 30000). Use for SPAs that hydrate after initial load. ## sw.web.search(query, opts?) Search the public web by keyword. Returns ranked result URLs + titles + snippets — no page bodies. Pair with `sw.web.scrape` when you want the full content of a specific result. const hits = await sw.web.search('cloudflare workers websocket hibernation', { count: 5 }) // hits = { // query: '...', count: 5, // results: [ // { url: '...', title: '...', description: '...', age: '2 days ago', source: 'blog.cloudflare.com' }, // ... // ] // } opts: count? — 1 to 20, default 10. country? — 2-letter code to bias results, e.g. 'us', 'gb', 'de'. freshness? — 'pd' (past day) | 'pw' (past week) | 'pm' (past month) | 'py' (past year). safesearch? — 'off' | 'moderate' | 'strict'. Default 'moderate'. ## When to use what - Find URLs by keyword → `sw.web.search`. Returns titles + snippets. - One page → `sw.web.scrape`. Returns markdown ready for an LLM prompt. - Search → scrape pattern is the standard agent loop: const hits = await sw.web.search(question, { count: 3 }) const pages = await Promise.all( hits.results.map((r) => sw.web.scrape(r.url)) ) - A whole site → not supported; scrape pages individually. ## Errors UPSTREAM_NOT_CONFIGURED — the platform doesn't have the matching provider wired in this environment. RATE_LIMITED — too many calls; retry with backoff. UPSTREAM_QUOTA — platform-wide quota exhausted; our team tops it up. UPSTREAM_ERROR — provider returned a non-2xx. For scrape, often means the page blocked the request or the URL is wrong. --- ## sw.push — Web Push notifications (sw.push) Available inside any deployed function via the sw argument (ctx still works as an alias). Web Push lets you send a notification to a user's browser even when your tab is closed. The browser registers a push subscription with its push service (FCM for Chrome, Mozilla autopush for Firefox, Apple for Safari), hands you back an opaque endpoint + crypto keys, and you POST encrypted payloads to that endpoint signed with a VAPID JWT. Per-project VAPID keypair: generated lazily on first call to sw.push.vapidPublicKey() or sw.push.send(). The same public key is what the browser must pass to PushManager.subscribe() — fetch it once in your service worker registration code and reuse it. ## sw.push.vapidPublicKey() Returns { vapid_public_key }. base64url uncompressed P-256 (87 chars). Hand this to the browser; the browser hands it to PushManager.subscribe. const { vapid_public_key } = await sw.push.vapidPublicKey() ## sw.push.subscribe(subscription, userId?) Stores a push subscription returned by PushManager.subscribe in the browser. Pass it through verbatim. user_id is optional — pass the app-user id if you want to target this person later. await sw.push.subscribe(subscription, user.id) // subscription = { endpoint, keys: { p256dh, auth } } // returns { id } Re-subscribing the same browser (same endpoint) refreshes the row, doesn't duplicate it. ## sw.push.unsubscribe(endpoint) Idempotent. Pass the endpoint string from subscription.endpoint. await sw.push.unsubscribe(subscription.endpoint) ## sw.push.send(payload, options?) Encrypts and POSTs the payload to one or many subscriptions. Returns { sent, failed, gone, recipients }. Options: user_id? — send to every subscription for one app-user endpoint? — send to one specific endpoint (idempotent retry) ttl? — push service TTL in seconds, default 86400 contact? — VAPID 'sub' claim (mailto: URL), default platform contact If neither user_id nor endpoint is given, broadcasts to every subscription in the project. Use sparingly. Subscriptions returning 404/410 are auto-deleted (browser revoked). // Send to one user await sw.push.send({ title: 'New message', body: 'You have mail.' }, { user_id: user.id }) // Send to a specific endpoint await sw.push.send({ url: '/orders/42' }, { endpoint: subscription.endpoint }) // Broadcast to whole project await sw.push.send({ title: 'Site update', body: 'New version live.' }) ## Browser-side flow 1. Get the public key from your function: const { vapid_public_key } = await fetch('/api/push-key').then(r => r.json()) 2. Register a service worker, then subscribe: const reg = await navigator.serviceWorker.register('/sw.js') const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapid_public_key, // base64url string }) 3. POST the subscription back to your function: await fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }) 4. Inside your function call sw.push.subscribe(sub, user.id). 5. In your service worker, listen for 'push': self.addEventListener('push', (e) => { const data = e.data?.json() || {} e.waitUntil(self.registration.showNotification(data.title, { body: data.body })) }) ## Limits Payload max 3000 bytes encrypted. Larger payloads throw. If you need to send more, store it server-side and push a tiny payload with the URL or id to fetch. --- ## sw.notifications — unified notify primitive (sw.notifications) One call → fan-out across push, in-app bell, and (when an address is provided) email. Safe for LLM tool-calling — the orchestration lives on the platform, the dev just says "tell user X this thing." ## sw.notifications.send(userId, { title, body?, url?, email?, channels? }) await sw.notifications.send(user.id, { title: 'Northern line: minor delays', body: 'Reported 14:02 GMT. Estimated 10–15 min impact southbound.', url: '/lines/northern', channels: ['bell', 'push'], // optional; default ['bell', 'push'] }) Returns { bell?, push?, email? } with per-channel result. Each entry is { ok: true, ... } or { ok: false, error: '...', code? } so the caller can branch without a try/catch per channel. Channels: - bell — writes a row to _notifications in the project's own database. Auto-creates the table on first use. Render with sw.notifications.list(userId) / unreadCount(userId). Hidden from db_describe / db_browse like every _-prefixed table. - push — proxies to sw.push.send against every active subscription for that userId. Silent no-op if the user hasn't subscribed. - email — only fires when opts.email is set explicitly. We don't auto-look up the address (your user-table shape is yours). Pair with sw.auth.fromRequest(req, { enrichFrom: 'members' }) to pull the email + send in two lines. ## Bell helpers const { notifications } = await sw.notifications.list(user.id, { limit: 50, // default 50, max 200 unread_only: false, // default false }) // notifications[i] = { id, title, body, url, read, created_at } const n = await sw.notifications.unreadCount(user.id) await sw.notifications.markRead(notifId) await sw.notifications.markAllRead(user.id) ## What this is NOT (yet) - No platform-level dedup, rate-limit, or per-user channel prefs. Roll those in your handler if you need them — this is the primitive. - No retries — push / email failures surface immediately. Re-call after fixing the cause. --- ## sw.image — Image transformations (sw.image) Available inside any deployed function via the sw argument (ctx still works as an alias). sw.image.resize builds a transformation URL — it doesn't fetch the image, it returns a URL that triggers transformation at the edge when the browser (or your code) fetches it. Stick the URL in for free, no extra hop. ## sw.image.resize(source, options) Returns a string URL. source: '/uploads/foo.png' — relative path on this project's domain 'https://example.com/foo.png' — absolute URL (must be reachable from the project's zone) options (all optional): width number pixel width height number pixel height fit 'cover' | 'contain' | 'scale-down' | 'crop' | 'pad' format 'auto' | 'webp' | 'avif' | 'json' (default: format=auto) quality 1–100 JPEG/WebP/AVIF quality dpr number device-pixel ratio (1, 2, 3) gravity 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'face' | '0.5x0.5' background '#ffffff' fill colour for pad/contain blur 1–250 sharpen 1–10 rotate 90 | 180 | 270 trim '20;30;20;30' edge trim (top;right;bottom;left) metadata 'keep' | 'copyright' | 'none' anim true | false (animated GIF/WebP support) Examples: const thumb = sw.image.resize('/photo.jpg', { width: 200, height: 200, fit: 'cover' }) // → https://your-project.somewhere.tech/cdn-cgi/image/width=200,height=200,fit=cover/photo.jpg const webp = sw.image.resize(uploadedFile.url, { width: 800, format: 'webp', quality: 80 }) return new Response(``, { headers: { 'Content-Type': 'text/html' } }) ## Billing 5,000 unique transformations / month free per project; usage beyond that is at /v1/pricing. Repeated fetches of the same transformed URL are cached at the edge — only the first fetch counts. Cached for 1 year by default. ## Source restrictions The source URL must be on the project's subdomain (default) or on a verified custom domain attached to the project. Cross-origin external URLs are blocked by default and return 403 — if you need to transform images from a domain you don't own, file via the `feedback` tool. For files in sw.fs, point at the project URL with the path: sw.image.resize('/api/files/photo.jpg', { width: 400 }) where /api/files/photo.jpg is a function that does sw.fs.read. ## Errors The transformed URL returns 4xx if Image Transformations isn't enabled on the zone, or 415 if the source isn't an image. The helper itself only validates the inputs and builds the URL — it does not pre-flight. --- ## Database — SQL support, capabilities, and limits (database-engine) ## Summary A managed relational database is allocated per project. The database is reached from deployed functions through `sw.db` and from outside the runtime through MCP, CLI, or REST developer surfaces. Query results use the platform result envelope: `data`, `count`, and `changes`. Supported SQL capabilities include: - SELECT / INSERT / UPDATE / DELETE / JOIN / GROUP BY / HAVING / window functions. - Constraints: PRIMARY KEY, FOREIGN KEY with ON DELETE/UPDATE behavior, UNIQUE, NOT NULL, CHECK. - Indexes, including B-tree indexes, partial indexes, and expression indexes. - Triggers: BEFORE, AFTER, and INSTEAD OF. - Transactions and savepoints. - JSON columns and JSON operators: `json_extract`, `->`, `->>`. - Full-text search. - Common Table Expressions, including recursive CTEs. Documented SQL differences and limits: - `RETURNING` is supported on INSERT. For UPDATE or DELETE, query back after the mutation when returned rows are needed. - ENUM should be represented with TEXT plus CHECK constraints. - UUID should be represented with TEXT and generated in function code with `crypto.randomUUID()`. - Stored procedures are not part of the runtime model; application logic belongs in deployed functions. - Materialized views are not part of the runtime model; cached tables can be refreshed through cron or jobs. - Writes are serialized per project. Reads are concurrent. Platform-provided database-adjacent responsibilities: - Semantic search through `sw.search.*`. - Database change webhooks through `sw.db.onchange`. - Per-user scoping through `sw.db.query(sql, params, { user })`, `sw.db.scoped(user.id)`, and `$current_user` sentinel flows. - A per-project write coordinator for ordered INSERT/UPDATE bursts. - Per-project database quotas defined by the current plan. - Portable database export through `sw.db.dump()`, MCP `db_dump`, and REST `/v1/db/dump`. Use the database for application data tied to accounts, content, sessions, orders, messages, embeddings, configuration, and ordinary relational state. Use a separate warehouse when the workload is primarily large analytical scans, hundreds of millions of rows, specialized database extensions, or cross-region multi-writer semantics. Related: `sw.db`, `portability`, `security-model`. --- ## Security model — who can read/write what (security-model) ## Three actor classes 1. **End users** of your app — sign up via `sw.auth`. Get a JWT in cookies or `Authorization: Bearer …`. Identified by `sw.auth.fromRequest(req)`. Cannot run raw SQL from the browser. 2. **Your server functions** — code you deploy under `api/*`. Run with project-level credentials. Can run raw SQL, hit any `sw.*` method, read env vars. 3. **You (the developer)** — somewhere.tech account, smt_ API key. Can deploy, change settings, query the database via MCP / dashboard, access logs. ## Per-user data access For data scoped to an end-user (notes, orders, messages, etc.) use `sw.db.scoped(user.id)` — every query the scoped client runs has `user_id = ?` injected automatically: ```js const user = await sw.auth.fromRequest(req); const db = sw.db.scoped(user.id); await db.query('SELECT * FROM notes WHERE id = ?', [noteId]); // → SELECT * FROM notes WHERE id = ? AND user_id = ? ``` No CREATE POLICY ceremony, no row-level security setup. The scope travels with the client object so a forgotten WHERE clause can't leak another user's row. The deploy-time scanner warns if it sees `sw.auth.fromRequest` + raw `sw.db.query` (without `scoped`) on a public route — that's the data-leak shape we catch automatically. ## Can raw SQL bypass scoping? - **From browser code:** no. The browser cannot call `sw.db.query` directly; only your server functions can. Browser requests reach the database only through a function you wrote. - **From your own server code:** yes, deliberately — you may need to run admin reports or cross-user queries. The scanner flags this as an advisory warning, not a block. Add `somewhere-security-allow` comment in the file when it's intentional. - **Via the MCP `db_query` tool:** yes — but only with your developer key (smt_), never with an end-user JWT. ## Deploy-time + on-demand review - **Every deploy** runs a regex scanner that catches the obvious shapes (auth bypass, raw SQL, payment metadata spoof, env leakage, eval, RCE). Hits surface as `warnings[]` in the deploy response — advisory only, never blocks. - **On-demand** (`security_review` MCP tool, also via dashboard) runs an LLM over the deployed code and returns a Markdown report with WHAT/WHERE/IMPACT/FIX per finding. Higher tiers use a stronger model; per-run cost is at /v1/pricing. ## Server-side secrets - `sw.env.SECRET_NAME` — server-only. Reading it inside a handler is fine. Returning it in a response (`Response.json(sw.env)`) is a leak; the security review flags this shape. - Stripe keys, API keys, model keys: set via `somewhere env set KEY value` or the dashboard. Stored encrypted. ## Admin paths - `sw.auth.admin` (privileged auth ops — impersonate, delete user, list all users) is not callable inside deployed functions — any call throws `AUTH_ADMIN_REMOVED_FROM_RUNTIME`. Use the developer-key surface instead (auth_user_update, auth_impersonate, …) from your own tooling. - `sw.db.migrate` runs DDL — gate it to your own user. - `sw.keys.*` / `sw.deploy.*` are developer-key only. ## Auth token lifecycle - Access tokens: 1 hour. Auto-refreshed inline — if expired and the refresh is valid, the response includes `X-New-Access-Token` + `X-New-Refresh-Token` headers (CORS exposes them). - Refresh tokens: 30 days, rotated on use, with a 24h grace window so a slow client doesn't get logged out from a race. - Revoke a session: `auth_revoke_session` (MCP) or `DELETE /v1/auth/sessions/:id` with a developer key. Related: `sw.auth`, `sw.db`, `common-mistakes`. --- ## Portability — getting your data out (portability) Ownership and exit surfaces are explicit. Three export paths: ## Full database dump ```bash # Via MCP (in Claude Code / any MCP client) db_dump({ project_id: "my-app" }) # Via the CLI (when shipped) somewhere db dump > backup.sql # Via curl curl -X POST https://api.somewhere.tech/v1/db/dump \\ -H "Authorization: Bearer smt_..." \\ -H "Content-Type: application/json" \\ -d '{"project_id":"my-app"}' \\ -o backup.sql ``` Returns a single `.sql` file: schema (CREATE TABLE / INDEX / TRIGGER) + every row as INSERT statements + transaction wrapper. Load it into any SQL database, or import through another SQL-compatible migration path. The platform never sees the `.sql` after handing it to the caller. Per-table cap of 1M rows in the in-worker dump; larger tables get a truncation warning appended. Contact support if you need a streaming dump above that. ## Per-table CSV ```bash db_export({ project_id: "my-app", table: "orders" }) ``` Up to 100K rows per call, optional filters. Useful for grabbing one specific table when you don't need the full dump. ## File storage ```bash fs_read({ project_id: "my-app", path: "/" }) # list a directory fs_read({ project_id: "my-app", path: "..." }) # one file # Or browse via the dashboard's Files tab and download via signed URLs. ``` ## Source code Your project's deployed functions live in the platform's file store. Pull them back via: ```bash somewhere pull # CLI clones into ./ ``` The code is standard JS/TS. The `sw.*` API is documented in `platform_help`; replacing it means swapping the binding for direct clients in the destination runtime. No proprietary build step or required framework is part of the function shape. ## What you cannot easily port - The somewhere.tech subdomain (`.somewhere.tech`) doesn't follow you. Custom domains do (you own the DNS). - End-user passwords are PBKDF2-SHA256 hashes according to the security reference. Destination compatibility determines whether reset is required. - The dashboard, copilot, and security-review surfaces are platform features — they don't migrate, but your data + code does. Related: `sw.db`, `billing`. --- ## Design System — Read before generating UI (design-system) > One reference for every agent writing frontend code on somewhere.tech. > Mirror at https://somewhere.tech/llms-design-system.txt (plain text, > identical content). Update the topic + the public file together. ## Rule zero: deploy raw source, don't build The platform compiles JSX/TSX on deploy. Never run `npm run build`, `vite build`, or any bundler before `somewhere deploy`. Push raw `.tsx` / `.css` / `.html`. Build output gets rejected with `BUNDLED_DEPLOY_REJECTED`. Visual editors (overlay annotator, `project_design_tokens`, `project_patch` find/replace, source-map resolution) all break against bundled output. ## The dev→tweak→promote loop Every project has a paired dev URL: `-dev.somewhere.tech` and `-dev.somewhere.site` serve the dev deploy slot. The dev URL is owner / invited-viewer only — invite via the preview-magic-link flow. Iterate on dev, run `project_promote` to publish the same code to prod (`.somewhere.tech`). The dev URL automatically force-gates even on free tier — public eyes never see in-progress design work. ## CSS variable convention (theme-correct by default) **The one rule: never hardcode a hex color in a component file.** Use CSS custom properties from `globals.css`. They auto-switch on ``. Bad — breaks in light mode: `
` Good — themes automatically: `
` The canonical var set (define both `:root` and `[data-theme="light"]` overrides in `globals.css`): | Token | Use | |---|---| | `--bg`, `--surface`, `--surface-2`, `--bg-2` | Page / cards / hover / code blocks | | `--text`, `--text-muted`, `--text-dim` | Body / labels / metadata | | `--border`, `--border-strong` | Dividers, emphasized dividers | | `--brand`, `--brand-deep`, `--brand-glow` | Primary CTA / hover / halo | | `--success`, `--warning`, `--danger` | Status pills | If a color you need isn't in the set, **add a var** rather than sprinkling raw hex. Pair every `:root` definition with a `[data-theme="light"]` override at the same time — otherwise the component silently breaks in light mode. ## Font pairing — defaults that work Default font stack (system-first, zero load, theme-clean): `font-family: -apple-system, system-ui, 'Segoe UI', Roboto, sans-serif;` If a project wants a display font, pair ONE display face with ONE neutral body face. Two safe pairings: - Display: Inter (heading 600–700) + body: Inter (400). Single family, two weights. Cheapest perf, no FOUT. - Display: Fraunces (serif, 600) + body: Inter (400). Editorial feel, still readable at small sizes. **Banned by default** (perf + readability traps): Roboto Slab, Open Sans (overused, looks generic), Comic Sans MS (obvious), Times New Roman (default browser fallback — pick something or use the system stack), Papyrus, Brush Script, anything calligraphic. For brand-led pages, override defaults at project level. Store the decision in `globals.css` as a single `--font-display` / `--font-body` pair, not scattered `font-family: …` declarations across components. ## Layout - Single content column on mobile (`max-width: min(100% - 2rem, 720px)`, centered). - Two-column from `768px` up via CSS grid, not flexbox-juggling. - Spacing scale (4-base): 4, 8, 12, 16, 24, 32, 48, 64 px — pick from this set rather than freehand pixel values. Store as `--space-1` … `--space-8` in `globals.css` for consistency. - Avoid sticky / fixed positioning except for the top nav. Sticky footers and pinned CTAs feel cheap. ## Animation - Hover: `transition: all 0.15s ease`. Anything slower feels laggy on buttons. - Page transitions: skip them. The dashboard / app surfaces should feel like native software, not a slideshow. - Loading states: a thin 1px progress bar in `var(--brand)` at the top of the surface reads as "we're working." A centered spinner reads as "we're stuck." ## Self-check before `somewhere deploy` 1. Toggle `data-theme="light"` on `` in DevTools — does every surface still read correctly? Any inline hex in a component `style={...}` is a near-certain failure. 2. Resize to 375px wide — does anything overflow horizontally? 3. Tab through with the keyboard — is the focus ring visible on every interactive element? 4. Visit the `-dev.somewhere.tech` URL after deploying — does the dev preview render the same as your local edit? (If not, you probably built instead of pushing raw source.) ## Design generation constraints Use concrete UI decisions. Keep theme values in CSS variables. Make variants differ along at least two axes when variants are requested, such as layout and type scale. Do not create variants that differ only by color. Related: `sw.image`, `deploy`, `dev-prod`, `portability`. --- # 10. Security practice reference ## 10.1 Authority model Every `/v1/*` request resolves to one authority. Route authorization is derived from that authority and the route capability declaration. | Authority | Actor | Capability boundary | |---|---|---| | anonymous | no credential | static files, public webhooks with signature verification, pre-login auth flows | | app_user | end user of an application | own rows/files in one project; no raw SQL; no project config; no other users' data | | runtime_project | deployed server function | application primitives for one project; no platform configuration | | internal_signed | platform delivery | HMAC-signed timestamped delivery | | developer_admin | project owner/developer | control of owned projects; audit logged | Admin/config routes are developer-only. Data routes are scoped to caller identity. ## 10.2 Data isolation - Each project gets its own database. - No cross-project query path is exposed. - Runtime credentials carry project identity and cannot spoof another project through body, query, or path parameters. - Tenant code runs in isolated edge runtime units without shared memory or a shared tenant filesystem. - Files are private by default and require visibility checks before serving. ## 10.3 Row-level ownership - App-user credentials cannot run raw SQL or batch SQL. - App-user credentials cannot enumerate tables. - App-user credentials cannot export or dump a table. - Scoped query paths rewrite or constrain operations to the current user. - Inserts force owner fields server-side where scoping is configured. - Updates and deletes add owner predicates where scoping is configured. - Complex queries should include explicit ownership predicates in function code. ## 10.4 Auth and transport - HTTPS/TLS is used for platform API and deployed app traffic. - Passwords are hashed with PBKDF2-SHA256, 100,000 iterations, and unique per-password salt. - App-user JWTs are signed with per-project secret material. - Developer dashboard sessions use HttpOnly cookies. - `smt_` developer keys are stored hashed at rest. - MFA/TOTP is available for end-user accounts. ## 10.5 Deploy review - A deterministic scanner runs during deploy compile. - AI security review is available according to plan. - Review categories include auth bypass, raw SQL/scoping problems, unsafe payment metadata, environment leakage, privilege escalation, RCE, email spoofing, conversation hijack, and CSRF. ## 10.6 Infrastructure - Compute runs on edge runtime infrastructure. - The database is managed per project. - File storage is object storage with platform access controls. - Backups and point-in-time recovery are platform responsibilities according to the security and database references. - Network-level protection and WAF behavior are provided at the platform edge. ## 10.7 Rate limiting and abuse controls - `sw.rateLimit` provides app-defined per-endpoint counters. - Platform API limits are plan-specific. - AI spend is constrained by balance and plan-specific warnings/limits. ## 10.8 Security program - Sensitive route policies are tested against authority classes. - Runtime keys cannot reach deploy, environment variable, key, domain, or billing surfaces. - Security fixes should receive regression coverage. ## 10.9 Reporting Use `platform_feedback({ message })` or the dashboard feedback channel for security reports. Include reproduction details and mark sensitive reports as sensitive in the message. ## 10.10 Compliance - Not intended for HIPAA-regulated protected health information. - Card data is handled by the payment processor and should not touch application servers. - Not set up for enterprise compliance arrangements, signed BAAs, SLAs, or data-residency guarantees today.