---
name: opencloud-build-apps
description: Build, validate, deploy, verify, operate, and troubleshoot static web applications on OpenCloud with its versioned JavaScript SDK, app-scoped CLI, PostgreSQL/REST, central Auth, Storage, private Realtime, Functions, cron, safe telemetry, and backups. Use for live OpenCloud app work or for an honestly offline-validated bundle when credentials are unavailable.
---

# Build apps on OpenCloud

Use this skill as the execution policy and <https://docs.opencloud.ai> as the
interface reference. Use the public, versioned OpenCloud CLI from
<https://github.com/opencloud-ai/cli>. The CLI uses the same control-plane
contract, server-side drafts, validator, deployment executor, and verification
operation as the platform. Do not inspect platform internals to infer
undocumented behavior.

## Select CLI or offline mode

Establish CLI execution capability before credentials.

The pinned CLI release for this skill is `v0.6.0`. Install it in an isolated
temporary directory and verify its release checksum:

```bash
OPENCLOUD_CLI_VERSION="v0.6.0"
OPENCLOUD_CLI_PACKAGE="opencloud-cli-0.6.0.tgz"
OPENCLOUD_CLI_DIR="$(mktemp -d)"

curl -fsSLo "$OPENCLOUD_CLI_DIR/$OPENCLOUD_CLI_PACKAGE" \
  "https://github.com/opencloud-ai/cli/releases/download/$OPENCLOUD_CLI_VERSION/$OPENCLOUD_CLI_PACKAGE"
curl -fsSLo "$OPENCLOUD_CLI_DIR/checksums.txt" \
  "https://github.com/opencloud-ai/cli/releases/download/$OPENCLOUD_CLI_VERSION/checksums.txt"

if command -v sha256sum >/dev/null 2>&1; then
  (cd "$OPENCLOUD_CLI_DIR" && sha256sum --check --ignore-missing checksums.txt)
else
  (cd "$OPENCLOUD_CLI_DIR" && shasum -a 256 --check checksums.txt)
fi

npm install --prefix "$OPENCLOUD_CLI_DIR" \
  --ignore-scripts --no-audit --no-fund \
  "$OPENCLOUD_CLI_DIR/$OPENCLOUD_CLI_PACKAGE"
OPENCLOUD_CLI="$OPENCLOUD_CLI_DIR/node_modules/.bin/opencloud"
test "$("$OPENCLOUD_CLI" --cli-version)" = "0.6.0"
```

Do this preflight before starting onboarding. If the
environment cannot execute a shell, Node.js 22/npm, HTTPS downloads, or the
verified CLI, it cannot deploy from the public CLI. Work honestly offline.

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

## Connect with the CLI

Check whether the CLI already has a ready `.opencloud/session.json`, or whether
`OPENCLOUD_API_URL` and `OPENCLOUD_TOKEN` are supplied.

- Work online when either credential source is ready. Start with `app list`.
- For a new project without a credential, ask for the user's email only if it
  is not already available. Once the title and visibility are agreed, run:

  ```bash
  "$OPENCLOUD_CLI" onboard \
    --email "$USER_EMAIL" \
    --name "$PROJECT_NAME" \
    --visibility private
  ```

  OpenCloud chooses the DNS-safe title-based slug and six-character random
  suffix. Never ask the user to find an available domain.

- For a new email, `onboard` creates a provisional passwordless identity,
  project, canonical HTTPS URL, and 24-hour account grant immediately. That
  grant can create additional apps with `app create`. Continue building while
  the user confirms the email within 24 hours.
- For an email that already exists, no credential or project is created until
  the user proves ownership using the emailed confirmation form. Ask them to
  confirm, then run `"$OPENCLOUD_CLI" onboard-complete`.
- The CLI stores its short-lived secret in `.opencloud/session.json`, creates
  an enclosing `.gitignore`, and forces mode `0600`. Use the CLI normally; do
  not open, print, copy, upload, summarize, or commit that session file.
- Use <https://opencloud.ai/start> only for legacy connection to an existing
  project or account management. Never ask for a password, browser cookie,
  bootstrap token, service-role key, or database credential.
- If verification is not completed within 24 hours, every app created under
  the provisional grant is paused. Runtime traffic, Functions, and cron stop;
  data, files, deployments, secrets, and backups remain intact. The app shows
  a verification-required page and resumes after the owner verifies.
- 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 pinned CLI from any directory:

```bash
"$OPENCLOUD_CLI" <command>
```

Session discovery searches parent directories, so commands work from nested
source folders. Run `"$OPENCLOUD_CLI" doctor` first for a redacted view of the
CLI version, session source, app identity, endpoint reachability, and deployed
platform version. Pass absolute app-directory paths when more than one app is
present.

## 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, Storage,
Realtime, Functions/cron, telemetry, or verification. Use
<https://docs.opencloud.ai/llms.txt> as the compact documentation index.

## Discover the assigned app

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

Treat `app get` as authoritative for `appUrl`, `authUrl`, and `apiUrl`.
App-scoped agents cannot create apps, manage credentials, delete apps, or
restore backups unless their credential and user role explicitly allow it.
Provisional account grants can create multiple new apps during their 24-hour
verification window:

```bash
"$OPENCLOUD_CLI" 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 sign-in/registration page; do not build a second password 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 browser
interaction contract, then deploy with a new version. Confirm the active
deployment before and after the change and leave the previous release available
for operator-authorized rollback. Preserve `runtime.javascriptSdk.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:

```bash
"$OPENCLOUD_CLI" init "$APP_DIR" \
  --app-id "$APP_ID" \
  --version "$VERSION"

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

"$OPENCLOUD_CLI" 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/
├── opencloud.verify.yaml
└── AGENT_REPORT.md
```

Use this manifest pattern:

```yaml
schemaVersion: 1
appId: 6f9619ff-8b86-4e6e-a62a-889950f42d3e
version: 2026.07.28-1
frontend:
  directory: frontend
  spa: true
runtime:
  javascriptSdk:
    version: 0.2.2
storage:
  authorization: owner-prefix
migrations:
  - id: 0001_create_items
    file: migrations/0001_create_items.sql
functions:
  - name: summarize
    entrypoint: functions/summarize/index.ts
    verifyJwt: true
cron:
  - name: hourly-summary
    schedule: "0 * * * *"
    function: summarize
    enabled: true
health:
  path: /
requiredSecrets:
  - AI_API_KEY
observability:
  metrics:
    - name: items_created
      type: counter
      unit: items
      dimensions:
        actor_type:
          values: [member, admin]
    - name: overdue_items
      type: gauge
      unit: items
```

Choose `storage.authorization: app` for a deliberately shared namespace or
`owner-prefix` for per-user files. Use a unique version for every deployment.
Pin an exact installed SDK version—never `latest` or a range. The CLI resolves
an omitted pin once while building an older author manifest, but generated
manifests should be explicit. Keep migration IDs ordered and append-only. Never
write migration checksums; the CLI computes them.

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`.

## Use the exact JavaScript SDK

Read the module path from runtime config:

```js
const runtime = await fetch("/_opencloud/config").then((response) =>
  response.json(),
);
const { createOpenCloudClient } = await import(runtime.javascriptSdk.module);
const opencloud = createOpenCloudClient();
```

The current exact import is:

```js
import { createOpenCloudClient } from "/_opencloud/sdk/js/v0.2.2/index.js";
```

Prefer runtime discovery in generators. Verify
`runtime.javascriptSdk.version`, `module`, and `types`; do not guess a latest
path. Runtime discovery returns the SDK pinned to the active deployment, so a
platform SDK release cannot move an existing app. TypeScript declarations are
served beside the module. Inspect the active pin without printing runtime
credentials:

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

Use only these public methods:

| Interface | Methods                                                                   |
| --------- | ------------------------------------------------------------------------- |
| Client    | `config()`, `session()`, `dispose()`                                      |
| REST      | `rest.request(path, init?)`                                               |
| Storage   | `storage.request(path, init?)`                                            |
| Functions | `functions.invoke(name, init?)`, `invokePublic(name, init?)`              |
| Realtime  | `realtime.channel(name, options?)`                                        |
| Channel   | `connect()`, `broadcast()`, `onBroadcast()`, `onStateChange()`, `close()` |
| Telemetry | `telemetry.summary()`, `telemetry.increment()`, `telemetry.gauge()`       |

There is no `functions.request`, `channel.on`, or `channel.subscribe`.

The SDK owns app identity headers, bearer tokens, cookie forwarding, and
refresh. The safe session exposes only user/profile and expiry metadata. Never
decode JWTs or persist token material.

## 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 SDK for REST:

```js
const response = await opencloud.rest.request(
  "items?select=id,title,created_at&order=created_at.desc",
);
if (!response.ok) throw new Error(await response.text());
```

Authenticated mode is the default. Use `{ auth: "anonymous" }` only for a
deliberately public RLS read.

## Use Storage, Realtime, Functions, and telemetry

For `owner-prefix`, start each decoded object name with the exact
`session.userId`, encode segments independently, and persist object metadata in
an RLS-protected table. Never request S3 credentials or use platform buckets.

Create private Realtime channels with a logical purpose:

```js
const channel = opencloud.realtime.channel("items");
channel.onBroadcast(({ event, payload }) => {
  if (event === "changed") void reloadItems(payload);
});
await channel.connect();
await channel.broadcast("changed", { reason: "item-created" });
```

Close channels on teardown. Send identifiers, not secrets or full records.

SDK `0.2.1` uses receiver-safe Realtime timers. Do not rebind browser globals.
Only a deliberately frozen `0.2.0` import needs its documented compatibility
shim; prefer the version selected by runtime config.

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

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

defineFunction(async ({ input, db, secrets, log, requestId, environment }) => {
  const body = await input.json<{ itemId?: string }>();
  if (!body.itemId) {
    throw httpError(400, "ITEM_ID_REQUIRED", "itemId is required");
  }
  const items = await db.from("items").select("id,title", {
    filters: { id: body.itemId },
  });
  log.info("item loaded", { itemId: body.itemId });
  return {
    items,
    requestId,
    environment,
    secretPresent: Boolean(secrets.get("AI_API_KEY")),
  };
});
```

`@opencloud/server` binds database, Auth, Storage, secrets, input, and logs to
the current app environment. 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.

Use browser `functions.invoke` for `verifyJwt: true` and `invokePublic` for
`verifyJwt: false`. Development Functions remain dormant until an explicit
`app dev invoke` command or deliberate preview interaction calls them. Dev
Functions get the isolated dev schema and no production secrets or
cron triggers. Before verification, explicitly invoke every Function declared by
the exact active revision with safe dummy input and inspect `app dev requests`;
the latest invocation of each must succeed. Repeat these checks after any sync.

Declare secret names in the manifest and provision values separately. For a
value OpenCloud may generate:

```bash
"$OPENCLOUD_CLI" secret generate "$APP_ID" INTERNAL_SIGNING_KEY
```

For a user-supplied provider key, create a one-time browser page:

```bash
"$OPENCLOUD_CLI" secret entry-link "$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.

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.

```js
await opencloud.telemetry.increment("items_created", 1, {
  dimensions: {
    actor_type: "member",
  },
  idempotencyKey: `item-created:${item.id}`,
});
await opencloud.telemetry.gauge("overdue_items", overdueCount);
```

Use an idempotency key for counter increments that may be retried. Browser
measurements are product signals, not trusted security evidence.

CLI `v0.6.0` provides `alert-rule` and `agent-feed` for configuring and
reading these signals. Do not bypass the protected session file. Prefer the
Agent Feed over raw logs or metrics. Alerts
inform the agent; they do not authorize automatic rollback, deletion, or
shared-platform repair. See the telemetry reference for the exact contract.

## Validate, deploy, and verify the real UI

Run app-local syntax checks/tests/build plus:

```bash
"$OPENCLOUD_CLI" validate "$APP_DIR"
```

Start the isolated development loop before changing production:

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

The capability URL has a separate migration-replayed schema and no production
data, secrets, Auth, Storage, Realtime, or cron. Functions only execute 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_CLI" app dev data "$APP_DIR" /rest/v1/items \
  --method POST --body '[{"title":"Preview item"}]'
```

Run the exact-revision verification and promote its receipt:

```bash
"$OPENCLOUD_CLI" app dev verify "$APP_DIR"
"$OPENCLOUD_CLI" app dev promote "$APP_DIR" \
  --idempotency-key "$IDEMPOTENCY_KEY"
```

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

`app dev promote` is the default completion path. It follows the durable
deployment, runs the authoritative feature-aware production verification,
prints the live URL, and stops dev only after success. The copied user prompt
authorizes promotion of the exact verified receipt; do not pause for another
confirmation. Dev is an iteration environment, not a finished result.

`app verify` remains available as a standalone durable release gate. It checks
active release state, the exact artifact and deployment-pinned SDK, canonical
HTTPS health, Chromium diagnostics, and the same app-declared primary-flow
contract used in dev. `app smoke` and `app verify-ui` are local diagnostics,
not substitutes.

For a product-specific gate, mark the contract as required:

```html
<meta name="opencloud-ui-contract" content="required" />
```

Then expose a bounded browser function before the page finishes booting:

```js
globalThis.__opencloudVerify = async ({ client, config, session }) => {
  document.querySelector("[data-open-workspace]")?.click();
  const ready = document.querySelector("[data-workspace-ready]");
  if (!ready) throw new Error("workspace did not render");
  return {
    passed: true,
    checks: ["opened workspace", `matched app ${config.appId}`],
    coverage: ["view-transition", "state-assertion"],
  };
};
```

The harness supplies the first-party client, exact runtime config, and safe
session. It accepts 1–20 short check names, applies a 10-second timeout, and
requires both `view-transition` and `state-assertion` coverage when the meta
contract or CLI flag requires interactions. It still fails on browser/network
diagnostics produced during the interactions.

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

## Run manifest-aware two-user verification

Use only the pre-provisioned verification user variables; never print them.
Place `opencloud.verify.yaml` beside the app manifest:

```yaml
schemaVersion: 1
data:
  mode: owner
  table: items
  ownerColumn: owner_id
  markerColumn: title
  insert: {}
storage:
  objectPrefix: opencloud-verify/items
realtime:
  topic: items
function:
  name: summarize
  secretName: OPENCLOUD_VERIFY_SECRET
  digestField: secretDigest
  presentField: secretPresent
cron:
  name: hourly-summary
```

Use `data.mode: owner` for isolation and forged-owner denial or `shared` when
both admitted users should see both fixtures. The verifier rejects unknown
fields and derives Storage authorization from `opencloud.yaml`. It validates
Function, secret, and enabled cron names against the manifest before live work.

```bash
"$OPENCLOUD_CLI" verify "$APP_ID" "$APP_DIR/opencloud.verify.yaml"
```

The verifier checks sessions, the declared data mode, manifest-derived Storage,
private Realtime, Function Auth and secret rotation, a deterministically
triggered cron invocation, logs, and usage. It cleans its fixtures.

## Recover only with explicit authorization

Create and list backups freely when requested. 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.

Never delete, archive, stop, roll back, or restore an app unless the user
clearly authorized that exact action.

## 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.

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