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:
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 privateOpenCloud 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,
onboardcreates a provisional passwordless identity, project, canonical HTTPS URL, and 24-hour account grant immediately. That grant can create additional apps withapp 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 mode0600. 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_URLonly 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,
.envcontents, secret values, or brokered access tokens.
Run the pinned CLI from any directory:
"$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:
- https://docs.opencloud.ai/getting-started/
- https://docs.opencloud.ai/getting-started/agents
- https://docs.opencloud.ai/sdk/javascript/
- https://docs.opencloud.ai/reference/manifest
- https://docs.opencloud.ai/guides/development
- https://docs.opencloud.ai/reference/verification
- 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
"$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:
"$OPENCLOUD_CLI" app create \
--name "Another project" \
--visibility privateUse 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:
"$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:
app/
├── opencloud.yaml
├── frontend/
├── migrations/
├── functions/
├── opencloud.verify.yaml
└── AGENT_REPORT.mdUse this manifest pattern:
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: itemsChoose 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:
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:
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:
"$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:
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:
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:
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:
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:
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:
"$OPENCLOUD_CLI" secret generate "$APP_ID" INTERNAL_SIGNING_KEYFor a user-supplied provider key, create a one-time browser page:
"$OPENCLOUD_CLI" secret entry-link "$APP_ID" AI_API_KEYGive 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:
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.
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:
"$OPENCLOUD_CLI" validate "$APP_DIR"Start the isolated development loop before changing production:
"$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:
"$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:
"$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:
<meta name="opencloud-ui-contract" content="required" />Then expose a bounded browser function before the page finishes booting:
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:
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-summaryUse 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.
"$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.