Agent instructions

OpenCloud build skill

---
name: opencloud-build-apps
description: Build, validate, deploy, verify, operate, and troubleshoot static web applications on OpenCloud through hosted MCP or the app-scoped CLI, with its versioned JavaScript SDK, bounded PostgreSQL data, central Auth, managed Files, native Web Push, private Realtime, Functions, background jobs, app email, cron, safe telemetry, and backups. Use for live OpenCloud app work or for an honestly offline-validated bundle when no online execution path is available.
---

# Build apps on OpenCloud

Use this skill as the execution policy and <https://docs.opencloud.ai> as the
interface reference. When a usable terminal is available, use the public,
versioned OpenCloud CLI at <https://github.com/opencloud-ai/cli> even if hosted
OpenCloud MCP tools are also exposed. Reserve MCP for surfaces that cannot run
the CLI or call the API, such as mobile-only ChatGPT or Claude sessions. MCP
and the CLI use the same control-plane contract, server-side drafts, validator,
deployment executor, and verification operation. Do not inspect platform
internals to infer undocumented behavior.

## Select CLI, MCP, or offline mode

Inspect the execution surface first. When a terminal is available:

1. Install and verify the pinned CLI release below.
2. If OpenCloud explicitly supplied an exact-app owner Agent session and its
   assigned app ID, use that session directly. Run `opencloud doctor` and
   `opencloud app get "$APP_ID"`; do not replace it with account login, create
   another app, or connect a different workspace.
3. Otherwise run `opencloud auth status`. Reuse a healthy account login or run
   `opencloud login` and let the user approve it in their browser. Run
   `opencloud app list`, select or create the requested app, and from its
   source directory run `opencloud app connect "$APP_ID"`.
4. Use the CLI for drafts, isolated development, deployment, verification, and
   operations. Do not switch to MCP merely because both interfaces are
   available.

When no usable terminal or direct API path exists and OpenCloud MCP tools are
present:

1. Call `get_opencloud_session`.
2. If it reports disconnected, call `connect_opencloud` for a returning user,
   or continue with `start_onboarding` for first registration.
3. For first registration, ask once for the email if necessary and call
   `start_onboarding` with the agreed project name and visibility.
4. A new email receives a provisional identity, first app, canonical HTTPS URL,
   and 24-hour grant immediately. The MCP server retains the grant; never ask
   for or expose its token. If the result is `provisional_ready` with
   `verification.emailSent: false`, continue the build without retrying
   onboarding and tell the owner that confirmation delivery is delayed.
5. Give the owner the returned `launchUrl` as the primary link while email
   confirmation or deployment is pending. The confirmation message names the
   project and explains that approval is required to continue using it. The
   launch page signs the confirming browser in and opens the project when the
   deployment is ready.
6. An existing email must approve the emailed request. Then call
   `complete_onboarding` with the returned onboarding ID. Its private
   completion token remains inside the MCP session.
7. Resolve the assigned app from the onboarding result or call `get_app`.
   Before selecting capabilities, make one monitoring decision: determine
   whether the product has an important failure that OpenCloud's built-in
   app, deployment, operation, cron, background-job, HTTP-error-rate, and
   telemetry-freshness signals cannot express. If it does, include
   `telemetry`; otherwise do not add custom monitoring merely because it is
   available. This is an agent decision, not a user approval step.
   Call `get_app_starter` with every capability implied by the request: `auth`,
   `data`, `files`, `functions`, `jobs`, `ai`, `email`, `notifications`, `realtime`, `telemetry`,
   `cron`, and/or `integrations`.
   Implement every returned `resolvedCapabilities` and `capabilityChecklist`
   entry. Use its current minimal manifest, SDK pattern, external E2E
   specification, and build invariants before authoring the first draft; call
   it again if the product scope grows.
8. Use source-draft, dev-session, verification, and promotion tools for all
   live work. Do not require the CLI as a second preflight.

## Understand how hosted MCP works

OpenCloud MCP is a stateful Streamable HTTP protocol, not a remote shell:

- Use `/build` on the canonical `mcp.<domain>` host for a native app-building agent. It exposes only onboarding,
  assigned-app inspection, draft, isolated dev, promotion, deployment
  evidence, and verification tools. Use the host root `/` for the full lifecycle on
  supported no-terminal connector surfaces. Terminal-enabled Codex, Claude
  Code, and similar agents use the CLI instead.
- `/mcp` and `/mcp/build` remain compatibility aliases; configure new clients
  with the canonical host root or `/build` endpoint.
- Initialization returns an opaque `mcp-session-id`. Reuse it on subsequent
  requests and deletion. A session belongs to the surface that created it;
  do not move one session between the full root and `/build` surfaces.
- The server retains only bounded onboarding state and a provisional grant
  created in that session. OAuth bearer credentials remain request-scoped.
  Neither credential is tool output, source content, or something to request
  from the user.
- Tools are authoritative actions. `opencloud-skill` is a supplemental MCP
  resource; some clients do not read resources automatically, so server
  instructions and `get_app_starter` carry the essential workflow too.
- Draft files live on OpenCloud. `expectedRevision` prevents overwriting a
  newer draft, and each `baseSha256` prevents overwriting a file that changed
  since it was read. On conflict, call `get_draft` plus `list_files` or
  `read_files` and rebase the intended change; never guess a revision/hash.
- `validate_draft` builds one immutable artifact from one source revision.
  Read `sourceManifest` and `sourceFiles` as author inputs, `artifactFiles` as
  packaged output, and every diagnostic `path`/`suggestedFix` before editing.
- A dev session materializes only a validated revision in an isolated
  database. Each later edit requires validation and `apply_dev_revision`.
  Synthetic data and explicit Function invocations produce evidence without
  touching production.
- `verify_dev_session` runs strict Chromium TLS/runtime checks plus the
  immutable `tests/opencloud.e2e.js` specification. Its receipt binds the
  exact revision, artifact, migration digest, and E2E source hash; any later
  edit invalidates the receipt. External tests run in independent child
  sandboxes with fresh migration-replayed data, Storage, Functions, and users.
  Up to five tests run concurrently by default; callers may request 1 through
  10 without changing the test artifact.
- Promotion and production verification return durable operations. Poll the
  matching `operation.id` with `get_operation` to a terminal state, then poll
  the matching verification. A queued operation or preview is not done.

Recover from failures by preserving evidence and changing the smallest
boundary:

| Failure                              | Correct recovery                                                                                 |
| ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| draft revision or file hash conflict | Re-read draft/files and reapply only the intended change.                                        |
| manifest diagnostic                  | Fix the reported source path; use `get_app_starter` when the manifest or SDK pattern is missing. |
| development migration failure        | Fix the named migration file/SQLSTATE, validate a new revision, and apply it.                    |
| Chromium certificate failure         | Treat it as an environment TLS failure; never disable certificate validation.                    |
| expired/stale receipt                | Reapply the exact current revision, repeat dev checks, and obtain a new receipt.                 |
| durable operation failure            | Report the terminal failure and diagnostic; do not promote or claim completion.                  |

Pasting a prompt cannot install an MCP connector. If tools are absent, state
that OpenCloud must be enabled on a supported agent surface. Claude Code and
Cowork can install the OpenCloud plugin; Claude.ai, Claude Desktop, and Claude
mobile can use the hosted connector. ChatGPT plugins and arbitrary MCP tools
are not currently available directly in ChatGPT mobile chats; mobile can steer
an already configured desktop task through ChatGPT Remote.

For every terminal workflow, establish CLI execution capability before
credentials.

The pinned CLI release for this skill is `v3.10.2` and requires Node.js 22 and
npm 10 or newer. The installer downloads that exact release, verifies its
published checksum, and installs the `opencloud` command for the current user:

```bash
curl -fsSL https://docs.opencloud.ai/install.sh | bash
test "$(opencloud --cli-version)" = "3.10.2"
```

CLI 3.10.2 requires explicit idempotency keys for app-owner Agent mutations
and supports schema-3 manifest `routes`, SDK 2.3.0 Function routes, and `app
domain get|add|check|remove`. Keep custom domains outside the manifest, reuse
an idempotency key only for the same intended mutation, and follow the returned
DNS and readiness observations before treating a domain as connected.

If the installer reports that its user-level binary directory is not on
`PATH`, apply the `export` command it prints before running the version check.

Do this CLI preflight before starting CLI login. If the
environment cannot execute a shell, Node.js 22/npm, HTTPS downloads, or the
verified CLI, it cannot deploy from the public CLI. If MCP is also unavailable,
work honestly offline.

Work offline only when both MCP and the verified CLI are unusable. Build and
locally validate a complete source tree, but do not claim live deployment or
verification.

## Connect with the CLI

An exact-app owner Agent session is already connected. OpenCloud supplies it
to AI SDK Codex harness v3 through `OPENCLOUD_SESSION_FILE` together with the
assigned app identity. Never open or print that file, run browser login, create
an app, or exchange the session for another credential. The session delegates
human-owner-equivalent authority for the exact assigned app: the Agent may use
every public CLI app operation, including deployment, production mutation,
rollback, restore, secret and access changes, archive, and deletion. It does
not need an additional confirmation merely because an operation is destructive
or affects production. Honor explicit user constraints such as “do not deploy”
and use ordinary product judgment, validation, and recovery.

That session cannot create another app, enumerate or operate another app,
change account settings, authorize or reveal account-owned provider
credentials, administer the platform, or access the host or shared
infrastructure. Do not infer this authority from merely running as an Agent.
Legacy AI SDK harness v1/v2 sessions and ordinary human-directed CLI sessions
retain their existing authority and approval rules.

Every nested deployment, backup, operation, credential, File, binding, grant,
or other resource is resolved back to its parent app before authorization. A
foreign ID and a nonexistent ID produce the same non-enumerating denial. Do not
probe either and never retry a boundary denial with account or platform
credentials.

For an exact-app owner Agent, “every app operation” includes:

- app inspection and supported lifecycle/configuration changes, including
  visibility, restart, archive, unarchive, and final deletion;
- draft and source-file management, isolated development, captured effects,
  validation, verification, deployment, active-artifact inspection, rollback,
  and inactive-deployment cleanup;
- production table and managed-File administration, including destructive
  row and File mutations;
- secret metadata, generation, secure set/replace, entry links, and deletion;
- backup creation, schedule, inspection, and restore;
- production Function invocation plus cron, job, queue, and app-email
  operations through their supported typed surfaces;
- logs, metrics, usage, visitors, alerts, Agent Feed, verification, and
  durable-operation evidence;
- app members, access grants, app credentials, and app-scoped access tokens;
  and
- listing, binding, configuring, using, and unbinding app integrations through
  typed app APIs.

Some app behavior intentionally remains reachable only through app code. For
example, enqueue through a declared Function rather than inventing an owner
queue bypass, and send email or execute a provider integration through its
typed Function client. Account connection creation/consent, `calling_user`
bindings, raw provider credentials, and unsupported platform features such as
ownership transfer are not app-owner CLI operations.

The complete owner-parity command tree is documented in the CLI reference for
the pinned public CLI `v3.10.2`. Confirm the exact version and command help
before using owner operations. Do not invent a missing command or bypass the
public CLI with a raw control-plane request; report an absent surface as a
product limitation.

For ordinary CLI use, account login is per OS user and app connection is per
source workspace:

```bash
opencloud auth status
opencloud login
opencloud app list

# Only when the requested app does not already exist:
opencloud app create --name "$PROJECT_NAME" --visibility private

opencloud app connect "$APP_ID"
opencloud doctor
```

- `opencloud login` creates a short-lived browser approval request, prints and
  opens its HTTPS URL, and polls while the user signs in with a one-time email
  link or configured password and explicitly approves the CLI. It does not
  start a localhost callback or ask the user to paste a code, email link,
  cookie, or token. Use `--no-browser` when the terminal cannot open a browser;
  give the user the printed approval URL.
- Approval returns a 15-minute account access token and a rotating 30-day
  refresh token. The account token may list, read, and create apps and connect
  a workspace; it cannot deploy, configure an app, read secrets, or perform
  owner-only actions.
- Secrets are stored under the OS credential service `ai.opencloud.cli` when a
  keyring is available. Headless environments fall back to a per-user,
  mode-`0600` file under `$XDG_CONFIG_HOME/opencloud/credentials` (normally
  `~/.config/opencloud/credentials`) on Linux,
  `~/Library/Application Support/OpenCloud/credentials` on macOS, or
  `%APPDATA%\OpenCloud\credentials` on Windows. Never open, print, copy,
  upload, summarize, or commit these credentials.
- `opencloud app connect` writes only a non-secret, ignored
  `.opencloud/app.json` binding in the source tree. It stores a separate
  24-hour app-scoped credential in the same protected credential backend. The
  CLI renews that credential from the account login when it nears expiry.
- `opencloud logout` (or `opencloud auth logout`) revokes the refresh-token
  family and all workspace credentials issued from it, clears the local
  account credential, current workspace credential, and resolved onboarding
  session, and retains non-secret workspace bindings for later reconnection.
  `opencloud login --force` replaces an unusable stored login.
- `.opencloud/session.json` and `opencloud onboard` implement passwordless
  first-project onboarding. `OPENCLOUD_API_URL` selects a non-default
  installation, and `OPENCLOUD_TOKEN` supplies explicitly delegated authority.
  Never inspect an onboarding session file or ask for a copied credential.
- OpenCloud chooses the DNS-safe title-based slug and random suffix for new
  apps. Never ask the user to find an available domain.
- Treat `OPENCLOUD_EDGE_URL` only as an optional CLI transport adapter. Preserve
  and report the canonical HTTPS app URL.
- Never invent IDs, URLs, credentials, secrets, operations, or results.
- Never print or commit tokens, passwords, cookies, `.env` contents, secret
  values, or brokered access tokens.

Run the CLI from any directory:

```bash
opencloud <command>
```

Workspace and legacy-session discovery search parent directories, so commands
work from nested source folders. `opencloud doctor` prints a redacted view of
the CLI version, credential backend, app identity, endpoint reachability, and
deployed platform version. Pass absolute app-directory paths when more than one
app is present.

## Use structured results and recover durable operations

The public CLI emits one stable machine-readable JSON result for every
successful command. Read fields from that document instead of scraping prose.
The owner-parity release also makes every failure structured, redacted JSON
with a non-zero exit status. Retain the error code, request ID, retryability,
operation ID, and bounded diagnostic; never retain credentials or secret
values.

After the owner-parity release is pinned, `app credential-create` requires a
new `--token-file` path, writes the one-time token there at mode `0600`, and
returns only redacted metadata plus the path in JSON. Never open, echo, trace,
or report that file. Its idempotency key is required. If an ambiguous response
leaves no file, repeat the exact request and key to replay the same credential;
never overwrite an existing token file. The exact-app owner session normally
needs no second app credential.

For every mutation, use one stable idempotency key for one intended effect and
reuse it after a timeout, disconnect, or lost response. An exact-app owner
Agent must pass it with every mutating command's `--idempotency-key` option;
the CLI rejects a missing key before creating a journal entry or request. A new
attempt with materially changed input is a new intended effect and gets a new
key. An artifact digest may identify input but is not sufficient by itself to
identify the intent. Never create a new key merely because the first response
was ambiguous. On `MUTATION_JOURNAL_AUTHORITY_MISMATCH`, do not delete journal
state or change keys to bypass recovery; inspect the exact operation and
current app state first.

Operation-starting commands follow their durable operation by default with a
bounded timeout. Use `--no-follow` only when deliberately detaching, and retain
the returned operation ID before doing other work. Recover without repeating
the mutation:

```bash
opencloud operation get "$OPERATION_ID" --follow
opencloud operation list "$APP_ID"
```

`operation get` accepts bounded interval and timeout options. A local wait
timeout says only that the terminal result was not observed; it does not mean
the mutation failed. Re-read the operation. Terminal failure or cancellation
exits non-zero and preserves typed diagnostics. After repairing input, start a
new intended effect with a new idempotency key. App deletion is special only in
lifecycle: the successful terminal result is returned before the app session
is revoked, and that session may read only its exact deletion receipt for a
bounded time afterward.

## Read only the docs you need

Read these first:

1. <https://docs.opencloud.ai/getting-started/>
2. <https://docs.opencloud.ai/getting-started/agents>
3. <https://docs.opencloud.ai/sdk/javascript/>
4. <https://docs.opencloud.ai/reference/manifest>
5. <https://docs.opencloud.ai/guides/development>
6. <https://docs.opencloud.ai/reference/verification>
7. <https://docs.opencloud.ai/guides/functions-cron>

Then read the capability page before implementing Auth, database, managed Files,
Web Push, Realtime, Functions/background jobs/cron, telemetry, or verification. Use
<https://docs.opencloud.ai/llms.txt> as the compact documentation index.

## Discover the assigned app

With MCP, use `list_apps` and `get_app`. Treat `get_app` as authoritative for
`appUrl`, `authUrl`, and `apiUrl`. Then call `get_app_starter` with the
assigned app ID and complete capability list. Treat
its resolved checklist as required. Use its files as a current scaffold, not
permission to overwrite intentional existing source.

With the CLI:

```bash
opencloud app list
opencloud app get "$APP_ID"
opencloud app origin "$APP_ID"
```

Treat `app get` as authoritative for `appUrl`, `authUrl`, and `apiUrl`.
An exact-app owner Agent can inspect and manage credentials, restore backups,
and delete its assigned app because its delegated role explicitly grants full
owner authority for that app. It cannot create another app or use any supplied
ID to cross the assigned-app boundary. Narrow app-scoped and legacy Agent
credentials retain their existing scope and action restrictions.
Provisional account grants can create multiple new apps during their 24-hour
verification window:

With MCP, call `create_app`. With the CLI:

```bash
opencloud app create \
  --name "Another project" \
  --visibility private
```

Use a private app when its UI or data requires a user. Let the edge redirect to
the central OpenCloud access page; do not build a second sign-in form.

## Change an existing app safely

For a change request, inspect and validate the existing bundle before editing.
Do not run `init`, replace the app ID, recreate the app, or rewrite an applied
migration. Preserve current behavior outside the request, append ordered
migrations for schema changes, update product tests and the required external
E2E specification, then deploy. OpenCloud assigns the new release name during
promotion; do not edit a legacy schema-2 top-level `version` merely to make an
update unique. Confirm the active
deployment before and after the change and leave the previous release available
for an authorized human owner or exact-app owner Agent to roll back. Preserve
`runtime.sdk.version`
unless the request explicitly includes an SDK upgrade; an upgrade is a new
release and must pass the complete product and browser gates.

## Make an immediate artifact checkpoint

Create a real manifest and non-empty frontend in the first coherent file batch:

With MCP, call capability-aware `get_app_starter`, then `create_draft` and
inspect its files.
Use `apply_file_changes` with the current draft revision and per-file hashes.
Include `opencloud.yaml` plus a non-empty configured frontend in the first
coherent batch, and call `validate_draft`. Read source/artifact file lists and
diagnostic fixes from the result before continuing. Grow the app in small coherent revisions;
use `apply_dev_revision` after each later validation.

With the CLI:

```bash
opencloud init "$APP_DIR" \
  --app-id "$APP_ID" \
  --version cli-v3-compat

opencloud artifact-check "$APP_DIR" \
  --expect-app-id "$APP_ID" \
  --max-files 4

opencloud validate "$APP_DIR"
```

Grow the product in small coherent batches. Re-run the checker after changing
manifest-reachable paths and validate after each runtime boundary.

## Author the deterministic bundle

Use this layout:

```text
app/
├── opencloud.yaml
├── frontend/
├── migrations/
├── functions/
└── AGENT_REPORT.md
```

Hosted MCP and schema-3-capable clients use this manifest pattern:

```yaml
schemaVersion: 3
appId: 6f9619ff-8b86-4e6e-a62a-889950f42d3e
frontend:
  directory: frontend
  spa: true
runtime:
  sdk:
    version: 2.2.0
files:
  access: user
  maxUploadBytes: 52428800
migrations:
  - id: 0001_create_items
    file: migrations/0001_create_items.sql
functions:
  - name: summarize
    entrypoint: functions/summarize/index.ts
    access: user
  - name: hourly-summary
    entrypoint: functions/hourly-summary/index.ts
    access: system
  - name: receive-support
    entrypoint: functions/receive-support/index.ts
    access: system
  - name: process-report
    entrypoint: functions/process-report/index.ts
    access: system
queues:
  - name: reports
    function: process-report
    concurrency: 2
    maxAttempts: 3
    retryDelaySeconds: 5
    retryBackoff: true
    timeoutSeconds: 120
email:
  addresses:
    - name: support
      displayName: Example Support
      function: receive-support
notifications:
  webPush: true
  icon: /icons/notification.png
cron:
  - name: hourly-summary
    schedule: "0 * * * *"
    function: hourly-summary
    enabled: true
health:
  path: /
secrets:
  INTERNAL_SIGNING_KEY: generated
  AI_API_KEY: required
  ORGANIZATION_LABEL: optional
integrations:
  calendar:
    provider: google-calendar
    account: calling_user
    cardinality: one
    capabilities:
      - calendar.events.read
  bank:
    provider: gocardless-bank-account-data
    account: calling_user
    cardinality: many
    capabilities:
      - bank.accounts.read
      - bank.balances.read
      - bank.transactions.read
observability:
  metrics:
    - name: items_created
      type: counter
      unit: items
      dimensions:
        actor_type:
          values: [member, admin]
    - name: overdue_items
      type: gauge
      unit: items
    - name: item_summarization_failures
      type: counter
      unit: failures
  alertRules:
    - id: overdue-items
      name: Overdue items detected
      metric: overdue_items
      aggregation: latest
      operator: gte
      threshold: 1
      window: 5m
      minimumSamples: 1
      severity: warning
    - id: item-summarization-failure
      name: Item summarization failure
      metric: item_summarization_failures
      aggregation: sum
      operator: gte
      threshold: 1
      window: 5m
      minimumSamples: 1
      severity: critical
```

Choose `files.access: app` only when authenticated members should share files;
keep the safer `user` default for per-user files. `files.maxUploadBytes` is an
integer from 1 byte through 100 MiB. Do not add a top-level release version to
schema 3; the publisher records it separately in the deployment database.
Pin the exact installed SDK version—never `latest` or a range. Keep migration
IDs ordered and append-only. Never write migration checksums; the CLI computes
them.

The pinned public CLI 3.10.2 defaults new versionless sources to schema 3 and
omits the top-level release version. Use `--version` only when intentionally
creating a legacy schema-2 source tree. Leave an existing schema-2 embedded
version stable across updates: the publisher ignores it for release naming and
assigns the database version. Immutable schema-2 artifacts remain readable.

Only the canonical manifest, configured frontend tree, declared migrations,
and declared Function source trees enter the archive. Inspect the exact file
list printed by `validate`.

## Define app routes and custom domains

Use schema 3 for `routes`. Assets are regular files relative to
`frontend.directory`; the two aliases below serve the same file. Function
routes require an explicit upgrade to the paired SDK 2.3.0, a declared `user`
or `public` Function, and explicit methods. Preserve older SDK pins when no
route-driven upgrade is needed.

```yaml
runtime:
  sdk:
    version: 2.3.0
functions:
  - name: pixel
    entrypoint: functions/pixel/index.ts
    access: public
routes:
  - id: favicon
    path: /favicon.png
    asset: icons/app.png
    access: public
  - id: apple-icon
    path: /apple-icon.png
    asset: icons/app.png
    access: public
  - id: pixel
    path: /pixel.jpg
    function: pixel
    methods: [GET, HEAD]
```

Only exact literal asset aliases may use `access: public` in a private app.
The exception does not expose the source file URL, directory, HTML, config,
data, or Functions. Other routes inherit app admission. Asset methods are
GET/HEAD; Function methods may also include POST, PUT, PATCH, and DELETE.
Routes cannot replace `/`, `health.path` with a Function, or reserved service
namespaces. Unmatched paths retain the existing static/SPA behavior.

Use whole-segment `:id`, optional `{/literal}` or `{/:id}`, a final optional
extension as `/report{.:ext}` or `/files/:id{.:ext}`, and a terminal named
wildcard such as `/downloads/*parts`. Omitted optional params have no key;
wildcards are arrays of one or more segments. Do not use `:id?`, regex,
unnamed wildcards, nested optionals, or required mixed segments. Paths are
case-sensitive and trailing slashes matter. Specificity chooses the path
before method checking; ambiguous overlaps fail validation and a disallowed
method returns 405 without falling through. See
<https://docs.opencloud.ai/guides/routing> for limits and complete examples.

For `/pixel.jpg?param1=test123&tag=one&tag=two`, use the SDK 2.3.0 handler's
`http.query.get("param1")` and `http.query.getAll("tag")`; `has` and ordered
`entries` are also available. `http.params` supplies decoded path values.
Neither query nor params merges into schema-validated `input`. GET/HEAD input
is `{}`; other methods retain JSON input. `http` is `null` for ordinary
Function calls. Do not invent raw Request/header/cookie access. HEAD runs the
handler and suppresses response bytes; keep GET/HEAD handlers free of write
side effects. Exercise the actual preview route, not only a named invocation.

Custom domains are owner app settings outside the manifest. One exact apex or
subdomain consumes the app's slot; `www` is separate. Use public CLI 3.10.2
`app domain get|add|check|remove`, App Settings → Domains, or full MCP
`get_app_domain`, `add_app_domain`, `check_app_domain`, and
`remove_app_domain`. CLI mutations use the exact-app journal and accept an
optional `--idempotency-key`; MCP mutations require an explicit
`idempotencyKey` of 8–200 characters. Reuse the key only with the same input
after an uncertain result. A confirmed owner is required, and `/build` does
not expose these tools. Copy the provided TXT
ownership and traffic records; use apex ALIAS/ANAME/flattening or published
A/AAAA alternatives. Preserve MX/TXT and unrelated records. Cloudflare
proxying can remain enabled from the start; report actual check failures,
not speculative proxy warnings. Add optional ACME delegation only from the
provided record. Never ask for the owner's DNS credentials.

Only a connected domain becomes primary; pending setup keeps the canonical
address. Private apps still sign in through central Auth; public apps retain
optional signed-in features through the SDK. On a custom hostname, the
same-origin sign-in bridge automatically continues in the same tab through
`auth.<base-domain>` and returns through a one-time, claim-bound callback. The
resulting host-only app-session cookie avoids another Auth round trip on later
app loads; the central Auth cookie is never shared with or readable by the
custom hostname. Email-link approval happens in the email window and session
completion resumes in the initiating Auth tab. Website domains do not change
email addresses or transfer browser storage and Web Push permissions. Removal
disconnects immediately; a different hostname can be added during cleanup,
while reuse of the retired hostname must wait. See
<https://docs.opencloud.ai/guides/custom-domains>.

Do not invent `opencloud.auth.signOut()`. A visible custom-domain Sign out
action sends a same-origin POST to `/_opencloud/sign-out`, then calls
`location.reload()` on success to clear in-memory SDK state and connections.
It revokes the current browser's parent session and aliases, retaining other
devices. Handle a failed response before reloading.

## Use the stable JavaScript SDK

Every frontend imports the deployment-pinned singleton from one stable path:

```js
import {
  opencloud,
  OpenCloudError,
  OPEN_CLOUD_SDK_VERSION,
} from "/_opencloud/sdk.js";
```

Do not fetch `/_opencloud/config`, import an immutable version path, construct a
client, or read runtime credentials in application code. The edge maps the
stable module and `/_opencloud/sdk.d.ts` declarations to the exact artifact
pinned by `runtime.sdk.version`. Inspect the active pin operationally
without printing runtime credentials:

```bash
opencloud app sdk-inspect "$APP_ID"
```

Use only these public methods:

| Interface | Methods                                                                                        |
| --------- | ---------------------------------------------------------------------------------------------- |
| Version   | `OPEN_CLOUD_SDK_VERSION`                                                                       |
| App       | `app.info()`                                                                                   |
| Auth      | `auth.currentUser()`, `auth.requireUser()`, `auth.signInUrl()`                                 |
| Data      | `data.table(name).list/getById/create/createMany/updateById/deleteById`                        |
| Files     | `files.upload/info/download/save/replace/remove/attach`                                        |
| Functions | `functions.call(name, input?)`, `functions.stream(name, input?)` for `user`/`public` Functions |
| Notifications | `notifications.status()`, `notifications.subscribe()`, `notifications.unsubscribe()`         |
| Realtime  | `realtime.subscribe(topic, handler)`, `realtime.publish(topic, event, payload)`                |
| Telemetry | `telemetry.summary/increment/gauge`                                                            |
| Client    | `dispose()`                                                                                    |

The SDK returns parsed values, managed file metadata, or typed streams—not raw
HTTP responses. It owns config discovery, app identity, Function access mode,
bearer tokens, cookie forwarding, refresh, file routing, and Realtime protocol
state. Never decode JWTs, persist token material, call raw REST/Storage/Function
endpoints, or construct buckets and object paths.

Handle expected failures through the typed error contract:

```js
try {
  await opencloud.data.table("items").create({ title });
} catch (error) {
  if (error instanceof OpenCloudError) {
    renderProblem(error.code, error.requestId, error.retryable);
  } else {
    throw error;
  }
}
```

An `OpenCloudError` has `code`, `surface`, `status`, `requestId`, `retryable`,
and optional `details`. Show a safe product message and retain the request ID
for diagnostics. Do not parse response text or expose raw platform details.

The browser SDK deliberately exposes high-level data and managed Files
operations instead of request paths. Use `opencloud.data.table(...)` and
`opencloud.files` with opaque file IDs. Never construct REST paths, Storage
buckets, object names, owner prefixes, URLs, or authorization headers.

## Build data with RLS

Write unqualified DDL; OpenCloud selects the app schema.

For owner-isolated records:

```sql
create table items (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid not null default auth.uid(),
  title text not null check (length(title) between 1 and 200),
  created_at timestamptz not null default now()
);

create policy items_owner_access
  on items for all
  using (owner_id = auth.uid())
  with check (owner_id = auth.uid());
```

For records shared by admitted app members, add a permissive business policy:

```sql
create policy items_member_access
  on items for all
  using (true)
  with check (true);
```

OpenCloud forces RLS and combines business policies with a restrictive app
boundary. Deployments execute the complete history in a disposable constrained
schema on the pinned PostgreSQL runtime before touching live app data.

Read <https://docs.opencloud.ai/reference/sql> before using nontrivial SQL.

Use the bounded data API. It validates identifiers, owns authentication and
parses the result:

```js
const items = opencloud.data.table("items");
const rows = await items.list({
  select: ["id", "title", "created_at"],
  orderBy: { column: "created_at", direction: "desc" },
  limit: 50,
});
const created = await items.create({ title: "Review evidence" });
await items.updateById(created.id, { title: "Reviewed evidence" });
```

The exact results are `list -> Row[]`, `getById -> Row | null`, `create ->
Row`, `createMany -> Row[]`, `updateById -> Row | null`, and `deleteById ->
boolean`. There is no `.rows` wrapper and no `insert`, `upsert`, or broad
mutation alias.

Reads use the current user when present and can follow deliberately public RLS.
Writes require a user. Mutate only through `updateById` and `deleteById`; never
build filters that can accidentally update or delete multiple rows.

## Query declared PostgreSQL searches

The unpublished SDK 2.4/platform candidate supports `data.search` inside
Functions. Pin `runtime.sdk.version: 2.4.0` and declare `data.search` entries
with a logical `name`, app `table`, selected columns including `id`, optional
equality `filterColumns`, and `fullText` and/or `vector` configuration. App
migrations create a `tsvector` column and/or `vector(n)` column and indexes;
the platform owns pgvector installation. See
<https://docs.opencloud.ai/reference/manifest#postgresql-search-declarations>.

```ts
const { hits } = await data.search("document-chunks", {
  mode: "hybrid", query: input.question, vector: queryEmbedding,
  model: "potion-128-v1", where: { document_id: input.documentId }, limit: 10,
});
```

Use `fullText` with query only, `vector` with vector/model, or `hybrid` with
both. Enforce the declared model/dimensions; re-embed stored content when the
model changes. Query text is bounded to 2,048 characters, results to 50 hits,
vectors to 2,000 finite float32-compatible dimensions with nonzero norm, and
filters to declared scalar equality columns. Search keeps caller Authorization
and RLS; private-file delegation adds no database authority. The browser SDK
has no search method; invoke a declared Function.

Show returned passages and separate full-text score/rank, cosine
distance/similarity/rank and hybrid `fusionScore`. Hybrid uses reciprocal-rank
fusion with k=60, not addition of unlike component scores. Vector retrieval is
exact; HNSW DDL is supported but approximate acceleration is not used. Never
present similarity as confidence or word highlighting as an embedding
explanation. Keep staged/obsolete chunks invisible through document-revision
RLS and invalidate app-derived results when the document workflow replaces or
deletes a source. Direct Files changes do not do that automatically.
Read <https://docs.opencloud.ai/guides/functions-cron#postgresql-search> for
limits, errors and indexing lifecycle before implementing this capability.

## Use Files, Realtime, Functions, background jobs, and telemetry

Upload a browser `Blob` or `File` directly. OpenCloud returns an opaque ID and
owns paths, authorization, size enforcement, names, content types, and
idempotency:

```js
const uploaded = await opencloud.files.upload({
  data: file,
  name: file.name,
  contentType: file.type || "application/octet-stream",
  onProgress: ({ percent }) => renderUploadProgress(percent),
});
```

`upload` and `replace` automatically retry one transient failure with the same
private idempotency key. Do not invent or manage a retry key in app code.

When a file belongs to a database record, prefer the compound helper. Create
the table with `file_id`, `file_name`, `file_type`, and `file_size` columns (or
provide an explicit `columns` mapping):

```js
const { file: storedFile, record } = await opencloud.files.attach({
  data: file,
  name: file.name,
  table: "item_attachments",
  values: { item_id: itemId },
});
```

`attach` reconciles an ambiguous metadata write and cleans up a definite
failure. If it throws `FILE_ATTACHMENT_INCOMPLETE`, retain the returned file
details for an explicit cleanup retry. Store the opaque ID; never store or
construct a bucket/object path, send Storage headers, or request S3
credentials. Use `download`, `save`, `replace`, and `remove` with the returned
file object or ID.

Subscribe to private Realtime topics with a logical purpose:

```js
const unsubscribe = await opencloud.realtime.subscribe(
  "items",
  ({ event, payload }) => {
    if (event === "changed") void reloadItems(payload);
  },
);
void opencloud.realtime
  .publish("items", "changed", {
    reason: "item-created",
  })
  .catch((error) => console.warn("Realtime notification skipped", error));
```

Call `unsubscribe()` when the view unmounts and `opencloud.dispose()` when the
app tears down. Realtime is best-effort invalidation, not durable truth. Check
`app.info().capabilities.realtime` before subscribing, never await publication
from a primary CRUD action or verifier cleanup, persist through `opencloud.data`
first, and let recipients reload durable truth. Send identifiers, not secrets
or full records. Query current controls again after rendering replaces nodes.

Write Deno-compatible Functions with the first-party server boundary:

```ts
import { defineFunction, errors, schema } from "@opencloud/server";

export default defineFunction({
  input: schema.object({ itemId: schema.uuid() }),
  handler: async ({
    input,
    user,
    data,
    secrets,
    log,
    requestId,
    environment,
  }) => {
    if (!user) {
      throw errors.unauthorized("SIGN_IN_REQUIRED", "Please sign in");
    }
    const item = await data.table("items").getById(input.itemId, {
      select: ["id", "title"],
    });
    if (!item) throw errors.notFound("ITEM_NOT_FOUND", "Item not found");
    log.info("item loaded", { itemId: input.itemId });
    return {
      item,
      requestId,
      environment,
      secretPresent: Boolean(secrets.get("AI_API_KEY")),
    };
  },
});
```

`defineFunction` accepts exactly `{ input, handler }`; `input` is already
parsed. Handler-only definitions and `input.json/parse/text` do not exist.
Use `ai.generateText` for completed text, `ai.streamText` for incremental UTF-8
text bytes, `ai.generateObject` with a `schema` for a validated object, or
`ai.generateImage({ prompt })` for PNG bytes. Return an `ai.streamText` result
in a no-store plain-text `Response` with `X-Accel-Buffering: no`, and consume it
with browser `functions.stream`. Text, stream, and object calls accept managed
PNG, JPEG, WEBP, non-animated GIF, or PDF Files as
`attachments: [{ type: "file", file: fileRef, detail? }]`; prefer this opaque
reference over downloading and copying bytes through the Function. PDFs are
limited to one managed File, 20 pages, and 20 MiB. Use
`{ type: "image", data: Uint8Array, contentType, detail? }` only for generated
or otherwise in-memory bytes. Remote URLs are not accepted. Image inputs are
limited to four and 15 MiB each; all attachments share a 20 MiB total.
Model selection, provider envelopes, idempotency, retries, and timeouts remain
private platform behavior.

When a Function reports its pinned SDK version, import
`OPEN_CLOUD_SDK_VERSION` from `@opencloud/server`; never hard-code the version
string.

The Function context contains `input`, `user`, `job`, `data`, `files`,
`ai`, `email`, `notifications`, `jobs`, `integrations`, `secrets`, `log`, `requestId`, and
`environment`. SDK 2.3.0 and later also provide `http`, the read-only route context or
`null` for an ordinary invocation.
These are high-level,
invocation-scoped capabilities; raw database clients, buckets, provider keys,
and bearer tokens are not exposed. The outer platform gateway allocates a
request ID before module loading and catches imports, rejected promises,
timeouts, invalid responses, and platform-call failures. Unknown production
errors are generic; dev diagnostics are bounded and redacted.

Declare browser access as `user` or `public` in the manifest. Declare every
cron, queue, inbound-email, or platform-only Function as `system`; the browser
SDK rejects it with `FUNCTION_SYSTEM_ONLY`. Call a browser Function with
`opencloud.functions.call(name, input)` for parsed output or
`opencloud.functions.stream(name, input)` for a byte stream; the SDK selects
the correct auth mode from the manifest.

Declare Postgres-backed background queues under `queues`; each targets a
declared `system` Function. Producers call
`jobs.enqueue(queue, input, { idempotencyKey, delaySeconds? })`, and every
enqueue requires a stable bounded idempotency key. Consumers receive the JSON
object as `input` and `{ id, queue, attempt }` as `job`. Delivery is
at-least-once: make side effects idempotent and throw `errors.unavailable` only
for retryable failures. `jobs.get(id)` reports `queued`, `running`,
`retry_wait`, `succeeded`, or `dead_lettered`. Queue payloads are
limited to 64 KiB, delays to seven days, and active work to 10,000 jobs per
app/environment/Function namespace. Terminal job status is retained for 14
days. Queue concurrency, attempts, delay, backoff, and timeout come from the
manifest and are pinned at enqueue. A suspended, archived, or transiently
non-active production app defers work without consuming an attempt. Removing a
queue declaration dead-letters its pending work with `QUEUE_NOT_ACTIVE`;
restoring the queue does not redrive it.
There is no strict FIFO, priority, manual acknowledgement, cancellation, or
dead-letter redrive API in the first release.

For private file processing, pin the paired SDK to `2.4.0` and enqueue from an
authenticated producer Function with explicit file inputs:

```ts
await jobs.enqueue("index-document", { fileId }, {
  idempotencyKey: `index:${fileId}:${revisionId}`,
  files: { read: [fileId] },
});
// In the declared system consumer:
const pdf = await files.download(fileId);
```

Keep `files.access: user`; delegation does not require sharing the library.
Pass one to sixteen UUIDs, at most 128 MiB aggregate; existing per-file limits
still apply. Selection is normalized for idempotency. The platform pins the
actual content generation: replacement, even with identical bytes, produces
`FILE_INPUT_CHANGED`; enqueue a new revision with a new key. Grants last at
most 14 days, with separate bounded access for each attempt. Logout alone does
not cancel them, but current file, principal, app and retained domain/token
access must remain valid. Delegation permits only downloading selected content,
not metadata, listing, mutation, further delegation or user impersonation.
Omit `files` for ordinary jobs and preserve their existing SDK pin.

System invocations keep `user` and `auth.uid()` null; an app-table RLS policy
must explicitly use `auth.is_system()` for rows that a queue, cron, or inbound
email consumer may access. Never use `auth.uid() is null` for this because it
also grants ordinary anonymous access in a public app. The platform separately
binds the signed system identity to the current app or development namespace.

Declare app-owned identities under `email.addresses`. A receive-capable alias
references a Function with `access: system`; that same allocated address is
used for sending and receiving. Function code sends only from a declared alias
with `email.send(message, { idempotencyKey })`. Provider credentials and SMTP
are never exposed. Development Functions capture every outbound message and
support synthetic `.test` inbound injection through the email CLI commands or
MCP operations.

Development Functions remain dormant until an explicit
`invoke_dev_function` MCP call, `app dev invoke` CLI command, or deliberate
preview interaction calls them. Dev
Functions get the isolated dev schema, isolated values for secrets declared as
`generated`, no owner-provided production values, and no cron triggers. A
deliberate enqueue automatically wakes its consumer in the same isolated
namespace. Before verification, exercise every Function through its intended
path: direct invocation or browser action for ordinary Functions, enqueue for
queue consumers, and synthetic injection for inbound email. Inspect
`list_dev_invocations` or `app dev requests`;
the latest invocation of each must succeed. Repeat these checks after any sync.

Declare native Web Push with `notifications.webPush: true` and SDK `2.1.0` or later.
Optionally set a same-origin `notifications.icon` as the app-wide default; if
it is omitted, OpenCloud supplies its logo.
Browser code inspects `notifications.status()` without prompting and calls
`notifications.subscribe()` only from a visible user gesture; never prompt on
page load. A Function sends to an authenticated app user with
`notifications.send({ userId, title, body?, path?, icon? }, { idempotencyKey })`.
Keep `path` and `icon` same-origin. A per-message icon overrides the manifest
default. Development captures visible notification payloads for
`list_dev_notification_captures` and never contacts a browser push provider.
For iOS acceptance, use Safari 16.4 or later and launch the web app after adding
it to the Home Screen. iOS and iPadOS currently ignore the notification payload
icon and display the installed app icon; provide that icon in the web app
manifest or an `apple-touch-icon`, which takes precedence when both exist.

For every brokered integration, creating or authorizing an account connection
and completing provider consent remain human account actions. With
`account: app`, an exact-app owner Agent may list the app's integration
declarations and current bindings plus bounded eligible-connection metadata
and opaque IDs for the exact app's declared slots, then bind, configure, or
unbind an already authorized connection through the owner-parity `integration`
commands after that CLI release is pinned. This app-context projection includes
only the delegated owner's connections usable by declared `account: app`
slots. It cannot enumerate the general account catalog, manage account
connections, expose raw credentials, reveal undeclared providers, include
`calling_user` connections, or cross into another owner's connections. A
`calling_user` binding always remains that calling user's action and authority.
Ordinary human-directed flows may continue to use App Integrations in the
browser.

For Google Workspace, declare a named `google-calendar`, `google-drive`,
`google-sheets`, `google-docs`, or `google-slides` integration slot with
`account: app | calling_user`, `cardinality: one | many`, and only its
provider-scoped read/write capabilities. Functions use the bounded typed
`integrations.calendar/drive/sheets/docs/slides(slot)` clients; raw Google
requests and `batchUpdate` bodies are not public APIs. A human connects one
reusable Google account in account Integrations and authorizes the available
resources; Calendar binds an exact calendar under the general binding rule
above. Never ask for, receive, store, log, or return an OAuth token, Google
subject, or raw provider response. Obtain an opaque connection ID only from
the exact-app projection above, never from conversation or the general account
catalog. Development uses deterministic synthetic Workspace data; production
needs an active release and active binding.

For read-only Google reporting on SDK 2.2.0 or later, declare
`google-analytics` with
`analytics.reports.read`, `google-search-console` with
`search.performance.read`, or `google-ads` with `ads.reporting.read`. These
slots use `account: app` and bind one exact GA4 property, verified Search
Console site, or directly accessible non-manager Ads customer. Functions use
the provider-independent `integrations.use(slot)` client and call the bounded
`analytics.reports.run`, `searchConsole.performance.query`, or
`googleAds.campaigns.report` operation. The broker validates the provider,
capability, binding, and input, so adding a future integration operation does
not require a core SDK release. Never accept a raw property, site, customer,
GAQL query, provider request, or token. Report ranges are bounded to 366 days,
and development returns deterministic synthetic data without contacting
Google.

The typed methods are Calendar `bindings/listEvents/createEvent`; Drive
`bindings/listFiles/getFile/createFile/createFolder`; Sheets
`bindings/getSpreadsheet/createSpreadsheet/getValues/updateValues/appendValues`;
Docs `bindings/getDocument/createDocument/appendText`; and Slides
`bindings/getPresentation/createPresentation/addSlide`. Pass an opaque
`bindingId` only to select among `cardinality: many` bindings.

For Asana, declare `provider: asana` and only the needed `asana.*`
capabilities for tasks, assignees, sections, custom fields, attachments,
stories/comments, and events. Every binding selects one exact project.
Functions use `integrations.asana(slot)` with bounded
`bindings/listTasks/getTask/createTask/updateTask`,
`listAssignees/assignTask`, `listSections/moveTask`,
`listCustomFields/updateCustomFields`,
`listAttachments/createExternalAttachment/uploadAttachment/downloadAttachment`,
and `listStories/createComment` methods. A section is a project grouping: a
list heading or board column. Never accept arbitrary Asana API calls, tokens,
workspace/project IDs from conversation, or tasks outside the bound project.
Task mutations that return current task state also require `asana.tasks.read`.
Task results expose only the bound-project membership and independently gate
assignee, section, and project custom-field details behind their read
capabilities. Development data is deterministic and synthetic.

Incoming Asana changes require `asana.events.receive`,
`asana.tasks.read`, `account: app`, and `events.function` naming a declared
`system` Function. The handler receives an `OpenCloudAsanaEvent` with
normalized events, up to 20 current contained task snapshots, and a
`tasksTruncated` marker. Use its `id`
idempotently; app writes can be observed again as incoming changes. Account
connection and exact-project authorization are human actions in OpenCloud; the
app binding follows the general rule above.
Installation setup requires an Asana OAuth app with Full Permissions because
not every endpoint has a granular scope; OpenCloud still enforces every
manifest capability and project boundary.

For HubSpot CRM, declare `provider: hubspot-crm`, `account: app`, and only the
needed `crm.contacts.read|write`, `crm.companies.read|write`,
`crm.deals.read|write`, `crm.owners.read`, `crm.pipelines.read`,
`crm.notes.write`, and `crm.associations.write` capabilities. Functions use
`integrations.crm(slot)`: contacts, companies, and deals expose
`get/search/create/update/upsert`; owners expose `list`; pipelines expose
`listDeals`; notes expose `create`; associations expose `create`. Association
writes also require the write capability for both source and target record
types. A human authorizes a reusable portal in account Integrations; its app
binding follows the general rule above. Never ask for a HubSpot token, accept
raw requests or a connection ID outside the exact-app projection, add delete
operations, invent webhook events, or use `account: calling_user`. Development
uses deterministic synthetic CRM data and does not contact HubSpot.

For open banking, declare `provider: gocardless-bank-account-data` with only
`bank.accounts.read`, `bank.balances.read`, and/or `bank.transactions.read`.
Functions use `integrations.bankAccountData(slot)` with bounded
`bindings/account/balances/listTransactions` methods. A human selects their
institution, completes bank-hosted consent, and authorizes exact accounts; the
app binding follows the general rule above. Never ask for bank login details,
GoCardless credentials, requisition IDs, account IDs,
or raw provider access. Development data is synthetic and no bank is contacted.

For Wise transactions, declare the technical provider
`provider: wise-balance-webhook`, `account: app`, and only
`payments.received.reconcile`. Functions use
`integrations.receivedPayments(slot).bindings/reconcile` for expected incoming
credits. The owner creates exactly one **Account deposit events** webhook in
Wise using the notification URL shown under account Integrations and tests it;
the app binding follows the general rule above. Do not ask them to create
Transfer update or Transfer issue events. Never request a Wise login, API token, OAuth grant,
certificate, balance, raw transfer ID, or raw transaction feed. Reconcile only
an exact expected amount/currency/time window; a result of `ambiguous` always
requires human review and must never trigger fulfillment.

For Slack, declare `provider: slack`, `account: app`, and only
`slack.messages.send` and/or `slack.messages.receive`. Receiving also declares
`events.message.function` targeting a declared `system` Function. Functions
use `integrations.slack(slot).bindings/sendMessage`; inbound handlers receive
`OpenCloudSlackMessageEvent`. A human connects the reusable workspace and
invites the managed `@OpenCloud` identity to an exact channel. The app binding
selects that channel and `mention` or `all_messages` under the general rule
above. Every outbound message is visibly labelled with the OpenCloud app name.
Inbound delivery is at least
once; use the stable event `id` to make side effects idempotent. Never ask for
a Slack token, handle raw provider events, choose arbitrary channels in code,
implement slash commands, or treat a Slack mention as authorization for a
sensitive action.

For Telegram, declare `provider: telegram`, `account: app`, and only
`telegram.messages.send` and/or `telegram.messages.receive`. Receiving also
declares `events.message.function` targeting a declared `system` Function.
Functions use `integrations.telegram(slot).bindings/sendMessage`; inbound
handlers receive `OpenCloudTelegramMessageEvent`. A human uses the app's
one-time link to add an exact private chat or group. The app binding selects an
owned paired chat and `directed` or `all_messages` under the general rule above.
`cardinality: one` selects one chat at a time; `many` can bind several. Private
chats need no bot administrator access. Directed groups keep the bot as a
member; all-message groups grant it minimal administrator access in that exact
group while Telegram privacy mode stays enabled. Every outbound message is
labelled with the OpenCloud app name.
Inbound delivery is at least once; use the stable event `id` to make side
effects idempotent. Never request a bot token or raw chat ID, handle raw
Telegram updates, choose a chat in code, disable privacy mode, or treat a
Telegram sender ID, mention, command, or reply as authorization for a
sensitive action.

Declare each Function secret in the manifest with one mode:

- `generated`: OpenCloud creates and retains a strong production value
  automatically and supplies an isolated synthetic value in development;
- `required`: the owner must enter a value before production; or
- `optional`: the owner may enter a value, but its absence never blocks a
  release and `secrets.get(name)` returns `undefined`.

Do not call a setup command for `generated` secrets. In an ordinary
human-directed or legacy Agent session, rotate one only when the user
authorized that change. An exact-app owner Agent may rotate an assigned-app
secret when its work requires it without seeking another operation-specific
approval. With MCP use `generate_secret`; with the CLI use:

```bash
opencloud secret rotate "$APP_ID" INTERNAL_SIGNING_KEY
```

For a `required` or `optional` owner-supplied value, call MCP
`create_secret_entry_link` or use:

```bash
opencloud secret configure "$APP_ID" AI_API_KEY
```

Give the returned URL to the user. Do not ask them to paste the value into the
agent conversation. Return only a presence flag, version marker, or one-way
digest for a secret.

Once the owner-parity CLI release is pinned, an exact-app owner Agent that
already has a value in a protected source may replace it with:

```bash
opencloud secret set "$APP_ID" AI_API_KEY \
  --idempotency-key "$IDEMPOTENCY_KEY" < "$PROTECTED_SECRET_FILE"
```

`secret set` reads the value only from standard input. Never place it in an
argument, JSON file, command history, tool output, or report. Reuse the same
idempotency key after an ambiguous response, then confirm only secret metadata.
If the human owner is the only source of the value, use `secret configure`
instead of asking them to reveal it to the Agent.

An AI Function uses the mode the owner chooses in App Integrations. The default
App-provided AI mode needs the owner to select an AI connection for that app;
the connection may be a Codex/ChatGPT device login or an OpenAI API key. If an
invocation reports `app_ai_credential_not_selected`, give the owner the
existing non-secret launch URL once and resume after that explicit consent
action; never substitute or copy the coding agent's credential. User-provided
AI instead requires every authenticated caller to connect a default AI
connection and explicitly allow the exact app in OpenCloud's trusted AI
permission window before its first AI request. Open
`/_opencloud/ai-consent?return_to=<encoded same-origin path>` only as a
top-level popup navigation initiated by the user's AI action; never fetch,
submit, frame, imitate, or auto-approve that Auth-origin form. Wait for the
`opencloud:ai-consent` result from that exact popup before invoking the
Function. The inference boundary rejects user-token use without the stored
app-specific grant. Cron, inbound-email, anonymous, and other system calls
cannot use AI in that mode. Keys and provider tokens remain inside the encrypted
integration vault and short-lived inference runtime; never declare them as app
secrets or ask for them in conversation. The owner also selects the app's text
model and default reasoning effort in App Integrations.

Use exact telemetry fields:

```js
const summary = await opencloud.telemetry.summary();
const rest = summary.activity.surfaces.rest;
const freshness = summary.activity.telemetry;
```

All six surfaces are always present. `usage` can be null. Treat unavailable,
missing, or truncated activity honestly; never label absence as healthy. Read
<https://docs.opencloud.ai/sdk/javascript/telemetry> for the exact response.

Define custom metrics only when they express a product or workflow signal the
platform cannot derive. Keep the catalog small and bounded. Never use user IDs,
emails, URLs, object keys, or arbitrary strings as dimensions.

Connect every custom repair signal end to end: declare the counter or gauge,
declare its matching `observability.alertRules` threshold in the same manifest,
and emit the measurement through the supported browser SDK at the real product
failure boundary. Use counters with `sum` for occurrences and gauges with
`latest` or `max` for stuck state or backlog. Do not invent a telemetry member
in the Function context, and do not add a fake failure solely to exercise an
alert example.

```js
await opencloud.telemetry.increment("items_created", 1, {
  dimensions: {
    actor_type: "member",
  },
});
await opencloud.telemetry.gauge("overdue_items", overdueCount);
```

Record a real browser-observed failure without replacing the original error:

```js
async function summarizeItem(itemId) {
  try {
    return await opencloud.functions.call("summarize", { itemId });
  } catch (error) {
    try {
      await opencloud.telemetry.increment("item_summarization_failures");
    } catch {
      // Keep the product failure as the primary error.
    }
    throw error;
  }
}
```

The SDK creates the private idempotency key and safely retries one transient
write. Application code supplies only the metric value and bounded declared
dimensions. Browser measurements are product signals, not trusted security
evidence.

For retained production notification history, use
`opencloud app notifications list <app-id>` and
`opencloud app notifications get <app-id> <message-id>`.
Use `opencloud operation list <app-id> --page` for paginated durable operation
history; continue with `--cursor` and the same filters and limit. Read command
help for supported filters. These reads use the existing exact-app session.

CLI `v3.10.2` provides `alert-rule` and `agent-feed` for operationally
configuring and reading these signals. For important app-owned failure modes,
define the bounded custom metric and its matching threshold under
`observability.alertRules` in the same manifest revision. Manifest rules are
validated before promotion and become active atomically with the deployment;
do not require a post-deployment rule-creation step for intended app behavior.
Use `alert-rule put` for an immediate operational rule or override against a
metric in the active deployment. If that rule becomes durable product intent,
move it into the next manifest revision and remove the operational override.
Do not bypass the protected credential store. Prefer the Agent Feed over raw
logs or metrics. Background queues contribute
`jobs.created`, `jobs.retried`, `jobs.succeeded`, `jobs.failed`, and
`jobs.active` signals, safe failure events, and a built-in failure alert. On
full MCP, use `list_background_jobs` and `get_background_job` for retained
per-queue and individual metadata. `list_background_jobs` accepts inclusive
ISO 8601 `from` and `to` creation-time bounds and a returned `nextCursor` for
pagination; time bounds apply to totals, queue rollups, and history. Payloads,
idempotency keys, cancellation, and redrive are intentionally unavailable. Alerts
inform the agent. An Agent may create an app-scoped custom-metric rule; each
threshold occurrence creates one deduplicated fire in the same conversation,
which stays unresolved until an exact-run repair deployment is verified.
An alert never widens a credential. An exact-app owner Agent already has
authority to choose rollback, deletion, or another owner operation for its
assigned app without another approval; narrower and legacy principals keep
their existing restrictions. Shared-platform repair always remains outside the
app boundary. See the telemetry reference for the exact contract.

## Validate, deploy, and verify the real UI

With MCP, call `validate_draft`, then:

1. `start_dev_session` for the exact validated revision; when the user asks to
   review the app before deployment, give them the returned
   `browserPreviewUrl` rather than the raw `previewUrl`. The browser entry
   requires an owner or builder login, then opens a clearly marked **Not live**
   development-preview window with Full size, Tablet, Mobile, and Reload tools.
   Its framed app uses isolated synthetic user A without replacing the user's
   normal OpenCloud session. An explicit no-deploy request stops at this review
   point and leaves the dev session active; it does not authorize promotion;
2. `apply_dev_revision` after every later coherent validated change;
3. `request_dev_app` to inspect the preview and REST reads;
4. `mutate_dev_data` only for isolated dummy fixtures, using the high-level
   `table`, `action`, `values`, and optional `id` fields. It authenticates as
   synthetic user A and preserves that identity through browser verification;
   never pass a REST path or user credential;
5. exercise each Function through its intended path: `invoke_dev_function` for
   ordinary Functions, enqueue through a producer for queue consumers, and
   synthetic injection for inbound-email handlers; then use
   `list_dev_invocations` for correlated diagnostics;
6. for app email, inspect `list_dev_email_captures` and
   `get_dev_email_capture`, then exercise every receive-capable alias with
   `inject_dev_email` and a reserved `.test` sender;
7. for Web Push, invoke its sending Function and inspect
   `list_dev_notification_captures` for the expected synthetic user, title,
   body, resolved icon, and same-origin click path; remember that iOS/iPadOS
   presents the installed app icon rather than the captured payload icon;
8. `verify_dev_session` for the exact active revision;
9. `promote_dev_revision` for that receipt;
10. `get_operation` until the durable deployment is terminal;
11. `verify_app`, its operation, and `get_verification_run` until every required
    production gate is terminal; and
12. `get_app` to confirm the active deployment and canonical HTTPS URL.

Make at most ten `verify_dev_session` attempts for one build. Use validation,
preview reads, and explicit Function evidence between attempts; after a tenth
failure, stop and report that structured result instead of looping.

With the CLI, run app-local syntax checks/tests/build plus:

```bash
opencloud validate "$APP_DIR"
```

Start the isolated development loop before changing production:

```bash
opencloud app dev start "$APP_DIR"
# after each coherent edit batch
opencloud app dev sync "$APP_DIR"
opencloud app dev request "$APP_DIR" /
```

The `start` result includes `session.browserPreviewUrl`, and `status` prints the
same value as top-level `browserPreviewUrl`. Give that URL to a human reviewer,
never the raw `previewUrl`. It opens the marked development-preview window and
its Full size, Tablet, Mobile, and Reload tools. If the user asked not to
deploy, leave the dev session running for review and do not run `app dev
promote`.

The capability URL has a separate migration-replayed schema. Auth, data,
managed Files, Functions, and background jobs are isolated from production; generated secrets
receive synthetic values. Owner-configured secrets, Realtime, runtime
telemetry, and cron are unavailable. For external E2E verification, each
`REQ-###` test gets a child sandbox with a fresh schema, managed Files
namespace, Function namespace, and three short-lived synthetic browser
sessions: owner A, admitted member B, and unrelated user C. The platform drops
those resources after the test even when the app's own UI cleanup fails.
Functions execute only when the CLI or a deliberate preview interaction calls
them.
Frontend-only syncs preserve dev data. A migration definition change resets
the dev schema and replays the complete ordered history. Treat every listed
unavailable capability as unavailable; never fall back to production.

Create isolated dummy fixtures without touching production:

```bash
opencloud app dev data "$APP_DIR" items create \
  --values '{"title":"Preview item"}'
```

For an email-capable app, exercise its dev-only mailbox without contacting the
production provider:

```bash
opencloud app dev email inject "$APP_DIR" \
  --to support --from customer@example.test \
  --subject "Test request" --text "Please acknowledge this message."
opencloud app dev email list "$APP_DIR"
opencloud app dev email get "$APP_DIR" "$MESSAGE_ID"
```

For a Web Push app, invoke its sending Function and inspect the captured
payload without contacting a browser push provider:

```bash
opencloud app dev notifications list "$APP_DIR"
```

Run the exact-revision verification and promote its receipt:

```bash
opencloud app dev verify "$APP_DIR" \
  --idempotency-key "$VERIFY_IDEMPOTENCY_KEY"
opencloud app dev promote "$APP_DIR" \
  --idempotency-key "$PROMOTE_IDEMPOTENCY_KEY"
```

Any source, migration, or production-base change invalidates promotion. Direct
`deploy` remains a lower-level automation path but is not the default agent
workflow because it has no dev verification receipt.

`promote_dev_revision` (or CLI `app dev promote`) is the default completion
path. Follow the durable deployment, run authoritative feature-aware production
verification, report the live URL, and stop dev only after success. The copied
user prompt authorizes promotion of the exact verified receipt for the general
builder flow, while an exact-app owner Agent session independently carries
standing authority for every same-app deployment and operation. Neither flow
adds another confirmation. Dev is an iteration environment, not a finished
result.

MCP `verify_app` (or CLI `app verify`) remains available as a standalone
durable release gate. It checks that the recorded deployment is still active,
re-hashes its immutable artifact and manifest, matches the pinned OpenCloud SDK,
and sends only a canonical `HEAD`. It does not load the production document,
run Chromium or `tests/opencloud.e2e.js`, create users, or access application
data; it writes only durable control-plane verification metadata. If a newer
deployment became active, verification refuses instead of following the moving
pointer. The full product gate is the exact-revision development receipt; CLI
v3 provides no partial local substitute for that.

Every new app must replace the starter's failing external spec with tests whose
titles begin with stable `REQ-###` IDs. Tests import exactly `test` and `expect`
from `@opencloud/test`, drive visible accessible controls, assert exact durable
create/reload/update/filter/navigation/delete outcomes where applicable, and
clean unique fixtures through the UI. Each test needs at least one trusted
action and one trusted assertion. The bounded runner supplies owner, admitted
member, second-owner-tab, and unrelated pages plus deterministic file fixtures;
it rejects skip/only, direct network/backend access, evaluate, routing, direct
navigation, and script injection. Test code runs in a blank network-isolated
controller and cannot read app globals, cookies, SDK clients, or response
bodies. The supplied `marker` is short and unique to the current REQ test
rather than shared with the other tests in the run. Use
`uniqueValue(prefix, maximumLength)` for bounded fields so the unique suffix
cannot be truncated, and `clickIfVisible(locator)` only for genuinely optional
cleanup controls.

The owner, member, and unrelated pages can all load only the isolated
development app. `unrelatedPage` is a distinct authenticated identity with no
implicit app-authored ownership or team membership; use it to challenge owner-
or team-scoped business RLS. Establish any app-domain membership through visible
UI. Do not expect `unrelatedPage` to be denied when the app intentionally shares
records with every admitted user through a permissive `using (true)` policy.
Private production verification separately requires the anonymous canonical
`HEAD` to redirect to Auth.

Each REQ test starts with fresh browser contexts and an independent runtime
sandbox. The scheduler runs up to five tests concurrently by default; the
verification request may set `parallelism` from 1 through 10. Role names accept bounded
regular expressions for accessible names, label/text/test-ID/placeholder
queries, and `filter({ hasText })`; bounded scalar matchers may supplement query checks,
but scalar checks do not replace the required page or locator assertion. Scope
duplicate labels through their dialog, form, or card. In `finally`, close open
overlays and wait for cleanup state to converge before interacting with a page
that another user changed. If cleanup also fails, the verifier preserves the
first primary command error and adds bounded browser/network/status context.
`selectOption` accepts a raw value and the bounded Playwright `{ value }`,
`{ label }`, and `{ index }` forms.

For controls created only after an interaction, assert the rendered phone target
with `expect(locator).toHaveMinimumSize({ width: 44, height: 44 })`. This catches
native checkbox and similar sizing defects that cannot be proven by static source
inspection alone.

Verification success receipts and structured failures include an attempt
report with revision/artifact/test hashes, requested parallelism, outcome,
failure classification, and phase timings. Per-test evidence separates sandbox
setup, browser execution, cleanup, and total time. If another request is still
verifying the same dev session, wait after `DEV_VERIFICATION_IN_PROGRESS`; the
platform preserves the active attempt instead of starting overlapping sandbox
cleanup.

Development verification independently fails primary-flow HTTP 404/405, mobile horizontal
overflow, unnamed visible controls, and primary touch targets below 44 by 44
pixels. Its private-app run uses short-lived admitted synthetic users and
removes their sessions, grants, and identities. Production retains only the
anonymous Auth `HEAD` redirect check.

Run every UI assertion in development. Managed Files, Functions, and background
jobs are available inside each disposable E2E
sandbox and use isolated namespaces on the ordinary capability URL. Realtime
and cron remain unavailable in dev and must
never fall back to production. There is no production E2E run. Realtime remains
best-effort and cannot block CRUD or cleanup; `opencloud.data` is the durable
source of truth.

Inspect deployments, error logs, usage, and cron history after normal traffic.

## Run manifest-derived release verification

The schema 2 or 3 manifest and immutable `tests/opencloud.e2e.js` artifact are the
development-verification inputs. Use the existing `@opencloud/test` API rather than
creating another test DSL or a local partial-verification configuration.

```bash
opencloud app verify "$APP_ID"
```

The durable production operation checks the recorded active release, exact
artifact digest and manifest, SDK metadata, and canonical `HEAD`. It does not
execute the external E2E specification. Use the isolated dev tools to exercise
Data, Files, Auth, and every declared Function before promotion; inspect cron
history, logs, and usage after normal production traffic.

## Apply the current authority boundary to destructive operations

Create and inspect backups before risky work when appropriate. Do not roll
back or restore merely to test a deployment. Code rollback replaces runtime
code/config but does not reverse migrations; database restore can discard
newer writes.

An exact-app owner Agent may delete, archive, stop, roll back, restore, mutate
production data or Files, rotate or delete secrets, and change access for its
assigned app without requesting another per-operation approval. Prefer
idempotent, recoverable commands, inspect the terminal operation, and verify
the resulting state. The app is the security boundary, not a reason to stop.

For ordinary human-directed sessions and legacy or narrower Agent credentials,
keep the prior rule: do not perform a destructive action unless the user
clearly authorized that exact action. No credential permits cross-app,
account, platform, host, or shared-infrastructure actions, and an explicit
user instruction not to deploy or mutate remains binding.

## Report completion honestly

For online work, record canonical URLs, IDs, exact validation/UI/verifier/log/
usage/cron outcomes, deployment state, observed friction, product limitations,
and confirmation that the app remains active—or the exact terminal deletion
receipt when deletion was the intended result.

For offline work, report the artifact digest and exact remaining online steps.
An offline-valid bundle is useful progress, not a deployed app.