Develop safely before production
OpenCloud development sessions let an agent see and exercise the exact app it is editing without deploying it over the production release.
Each session has:
- a stable, unguessable preview URL;
- an authenticated
browserPreviewUrlthat an owner or builder can open to review the current revision in a clearly marked, responsive preview window without deploying it; - an isolated PostgreSQL schema built by replaying the app's canonical migrations;
- immutable frontend and Function revisions;
- correlated browser, HTTP, Function, and background-job diagnostics; and
- a verification receipt bound to the exact artifact, migrations, and production base deployment.
The preview cannot read production data or owner-configured secrets. Auth, managed Files, Functions, Web Push capture, and background jobs use isolated development state; generated secrets receive synthetic values. Realtime, runtime telemetry, and cron are unavailable. During external browser verification, OpenCloud creates a disposable child sandbox for each test with its own database schema, managed Files namespace, Function namespace, and synthetic users. Development Functions remain dormant until an explicit CLI command or deliberate preview interaction invokes them. A deliberate Function enqueue then wakes its declared system consumer automatically inside the same isolated namespace.
The browser preview entry uses the reviewer's normal OpenCloud login only to check owner or builder access. It then sets a host-only, short-lived session for synthetic user A on that one dev hostname. It does not replace the reviewer's normal oc_session. Reopen browserPreviewUrl when the preview session expires. The link opens a persistent Development preview — Not live shell with Full size, Tablet, Mobile, and Reload controls. The isolated app runs on its dev origin inside a sandboxed frame, so app navigation cannot remove the warning shell; viewport controls resize only that frame. The raw previewUrl remains the capability origin for bounded agent inspection and may show AUTH_REQUIRED when opened directly by a person.
Start the loop
For a routed app, validate and apply each changed route table together with its targets. Public CLI 3.10.3 validates and bundles routed schema-3 manifests, including SDK 2.3.0 Function routes.
Exercise routed Functions through their actual preview path, including query values, optional segments, repeated query keys, and allowed/denied methods. An ordinary named Function invocation has http: null and does not verify route matching. Routed preview calls use the isolated revision's Functions, data and captured effects. Public production icon aliases do not remove the development preview's access boundary. Custom domains never point at a dev session; use its preview URLs.
From the app source directory:
opencloud validate "$APP_DIR"
opencloud app dev start "$APP_DIR"start creates a source draft, synchronizes only changed files, runs the authoritative canonical validator, applies migrations to a dev-only schema, and returns both the raw capability URL for agent inspection and the browserPreviewUrl for human review. It stores only IDs and hashes in .opencloud/dev.json; no credential is copied into that file.
Edit locally, then update the same preview:
opencloud app dev sync "$APP_DIR"
opencloud app dev request "$APP_DIR" /
opencloud app dev status "$APP_DIR"Frontend-only changes retain dev data. When the ordered migration definition changes, OpenCloud drops the dev schema and replays the complete migration history from empty. This makes migration drift visible without touching production. Add dummy records through the preview UI or the dev-only fixture command. OpenCloud authenticates fixture writes as synthetic user A so normal RLS and auth.uid() defaults apply; do not put test fixtures in production migrations:
opencloud app dev data "$APP_DIR" events create \
--values '{"name":"Preview workshop","capacity":12}'The browser runtime config says environment: "dev" and lists unavailable capabilities. Preview responses use Cache-Control: no-store.
Test Functions explicitly
Use the first-party server library in every new Function:
import { defineFunction, schema } from "@opencloud/server";
export default defineFunction({
input: schema.object({ eventId: schema.uuid() }),
handler: async ({ input, data, requestId, environment, log }) => {
const event = await data.table("events").getById(input.eventId, {
select: ["id", "capacity"],
});
log.info("capacity checked", { eventId: input.eventId });
return { event, requestId, environment };
},
});@opencloud/server supplies the exact input, user, job, data, files, ai, email, notifications, jobs, integrations, secrets, log, requestId, and environment context. The platform's outer runtime boundary allocates a request ID before loading user modules and catches import errors, rejected promises, timeouts, invalid responses, and platform-call failures. Production returns a safe generic message for unknown errors. Development returns bounded diagnostics with secret-like values redacted.
Invoke a dev Function only when its side effects are intended:
opencloud app dev invoke "$APP_DIR" capacity \
--body '{"eventId":"00000000-0000-4000-8000-000000000001"}'
opencloud app dev requests "$APP_DIR"The invocation gets the dev schema and isolated synthetic values only for manifest secrets declared as generated. It cannot inherit owner-provided production values or cron triggers; required owner values remain unavailable and absent optional values resolve to undefined. Its response, duration, request ID, and redacted error are recorded for the session. Before verification, exercise every Function through its intended path with safe dummy input: direct invocation or browser action for ordinary Functions, enqueue for queue consumers, and synthetic injection for inbound email. The latest invocation of each must succeed. A source sync creates a new immutable revision, so repeat these invocations after every sync that you intend to verify.
The preview frontend may call the normal browser SDK opencloud.functions.call(...) or stream(...). OpenCloud routes that deliberate call to the current dev Function revision and records it as browser-dev; merely loading the preview never boots a Function.
Background queues are available in the ordinary dev session and in every E2E sandbox. Enqueue through a Function, then assert durable application state from the consumer rather than sleeping for a fixed duration. Queue payloads and status are scoped to the exact dev Function namespace and are deleted with the session or sandbox. Cron remains disabled; a queue is not a cron substitute.
Test application email without delivery
A Function bound to a development session always captures outbound email. The configured production provider is never contacted. Captures retain bounded recipient and body data plus attachment metadata and digests, not attachment bytes.
With the CLI:
opencloud app dev email inject "$APP_DIR" \
--to support --from customer@example.test \
--subject "Round-trip test" --text-file fixtures/request.txt \
--attachment fixtures/receipt.pdf
opencloud app dev email list "$APP_DIR"
opencloud app dev email get "$APP_DIR" "$MESSAGE_ID"Body and attachment paths resolve relative to the app directory. With MCP, use list_dev_email_captures, get_dev_email_capture, and inject_dev_email for the same workflow.
Synthetic injection accepts only reserved .test sender and Reply-To identities. It exercises normal handler routing, and any reply is captured in the same session. Captures and injected messages are deleted when the session stops or expires; this path does not test public DNS or MailPace signatures.
Test Web Push without delivery
A Function bound to a development session captures every notifications.send(...) payload instead of contacting Apple, Google, Mozilla, or Microsoft push services. Invoke the sending Function, then inspect the active session with the CLI:
opencloud app dev invoke "$APP_DIR" send-notification \
--body '{"title":"Preview reminder","path":"/reminders/1"}'
opencloud app dev notifications list "$APP_DIR"With MCP, use invoke_dev_function and list_dev_notification_captures. Each record includes the synthetic user, visible title and body, resolved icon, same-origin click path, and timestamp. Captures are deleted with the session. This verifies the manifest, Function SDK, payload, fallback-icon resolution, and click target; it does not replace a real-browser acceptance test for permission, provider delivery, or OS-specific presentation.
Declare external browser flows
New apps put their product-level browser tests in tests/opencloud.e2e.js, outside the served frontend:
import { test, expect } from "@opencloud/test";
test("REQ-001 creates and reloads an event", async ({
page,
uniqueValue,
clickIfVisible,
}) => {
const title = uniqueValue("Browser event", 60);
await page.getByRole("button", { name: "Create event" }).click();
const dialog = page.getByRole("dialog", { name: "Create event" });
try {
await dialog.getByLabel("Event title", { exact: true }).fill(title);
await dialog.getByRole("button", { name: "Save", exact: true }).click();
await expect(page.getByText(title, { exact: true })).toBeVisible();
await page.reload();
await expect(page.getByText(title, { exact: true })).toBeVisible();
} finally {
await clickIfVisible(
page.getByRole("button", { name: "Cancel", exact: true }),
);
}
});Use stable REQ-### titles for requested outcomes. Each test must execute a visible action and a trusted assertion. The bounded fixtures include owner, member, second-owner-tab, and unrelated pages, deterministic PNG/PDF/text uploads, a short unique marker per test, uniqueValue(prefix, maximumLength) for bounded form/database values, clickIfVisible(locator) for optional cleanup controls, and response-status assertions. uniqueValue preserves the unique suffix rather than letting an app or database truncate it. The controller is network-isolated and cannot read app globals, cookies, SDK clients, or backend responses. Bundling rejects skip/only, direct networking, backend routes, evaluation, routing, direct navigation, and script injection.
All three synthetic identities can load the test app in development and production. unrelatedPage has no implicit app-authored ownership or team membership, so use it to challenge owner- or team-scoped RLS after establishing any intended membership through visible UI. The production verifier checks anonymous private-app denial separately.
Each test receives fresh browser contexts and a fresh runtime sandbox created from the exact migration history. Up to five tests run concurrently by default. API and MCP callers may set parallelism from 1 through 10 on verify_dev_session; operators may change the default with OPENCLOUD_E2E_PARALLELISM. Scope duplicate control names through their dialog, form, or card. In finally, close overlays and wait for the cleanup page to reflect changes made by another page before clicking its controls. Accessible names, label/text/test-ID/placeholder queries, and filter({ hasText }) may use bounded regular expressions. Scalar assertions on query results are supported, but do not replace the required locator or page assertion. OpenCloud preserves the first primary command error if cleanup also fails and includes bounded browser/network/status context in diagnostics.
Use selectOption("stable-value") when the option value is part of the app contract. The bounded Playwright forms selectOption({ label: "Visible name" }), selectOption({ value: "stable-value" }), and selectOption({ index: 1 }) are also supported.
The exact spec source/hash is stored with the materialized dev revision and bound into its receipt. Production verification reopens the recorded artifact and runs the same hash. See Verification contract for the complete fixture and locator API.
Run the same real UI flow in dev and production where possible. App code uses the deployment-pinned opencloud singleton: opencloud.data.table(...) for rows and opencloud.files.upload, attach, download, replace, save, and remove for managed Files. File IDs are opaque. Never construct REST paths, Storage buckets, object names, owner prefixes, URLs, or authorization headers. Managed Files and background jobs are available inside the isolated E2E sandbox when declared by the manifest, and use the parent session's isolated namespaces during ordinary development. Realtime and cron remain unavailable. Do not substitute a production call.
Capability fixtures still require deletion through the visible UI in finally, because the same immutable test also runs against production. The dev platform teardown is a safety net, not a replacement for lifecycle tests.
Then run:
opencloud app dev verify "$APP_DIR"A successful receipt is invalidated by any later source change, migration change, or production deployment.
Promote the exact verified revision
opencloud app dev promote "$APP_DIR" \
--idempotency-key "$IDEMPOTENCY_KEY"Promotion does not rebuild from the working directory. It deploys the exact validated draft revision named by the unexpired receipt. If production changed after the dev session started, or the draft changed after verification, promotion refuses and requires a fresh verification path.
By default the promote command follows the deployment, runs the same feature-aware contract against production, prints the live HTTPS URL, and stops dev only after every gate passes. On failure it leaves dev intact for another iteration. Use app dev receipts or app dev evidence to inspect exact-revision evidence after cleanup.
Session lifecycle
Sessions expire after 24 hours. Verification sandboxes expire after at most 15 minutes and are normally removed immediately after their test. Cleanup drops the test schema, Storage objects and bucket, Function links, synthetic Auth sessions/users, and control-plane row. The worker retries retained sandboxes after a crash. Stop a finished parent session earlier to release its resources immediately.
Current limitations are explicit: there is no production-data clone, Realtime sandbox, automatic cron trigger, or automatic Function boot. Each external test's database starts from migrations rather than the mutable parent dev data. The verifier provisions three short-lived synthetic browser users per test (owner A, admitted member B, and unrelated C), an isolated managed Files namespace when declared, and removes them afterward. There is no production-secret access or runtime telemetry. Unavailable capabilities are never silent fallbacks to production.
