From 930b5da1112b51f6a257474158c11f3ae5275670 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 04:19:41 +0000 Subject: [PATCH 1/4] docs(deployment): Cloud Connection operator page for a self-hosted server A new page under content/docs/deployment/ walks a self-hosted operator through connecting their server to an ObjectStack Cloud account from Setup -> Apps -> Cloud Connection: what the connection gives, the prerequisites, the connect steps and the panel's states, what the server stores, rename and disconnect. Every server-side statement is read from packages/cloud-connection (and the pinned objectui panel) or measured on a showcase dev boot against a stand-in control plane. Stable anchors: #connect, #rename, #disconnect. One-line cross-links from the three pages that already name @objectstack/cloud-connection. Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude --- content/docs/deployment/cli.mdx | 2 + content/docs/deployment/cloud-connection.mdx | 227 ++++++++++++++++++ content/docs/deployment/meta.json | 1 + .../docs/protocol/kernel/metadata-service.mdx | 2 + content/docs/ui/setup-app.mdx | 2 +- 5 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 content/docs/deployment/cloud-connection.mdx diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx index d68e1654d6e..bd35864b43e 100644 --- a/content/docs/deployment/cli.mdx +++ b/content/docs/deployment/cli.mdx @@ -2794,6 +2794,8 @@ A `404` means the target runtime does not mount `MarketplaceInstallLocalPlugin` (from `@objectstack/cloud-connection`). The endpoint is opt-in, so a runtime composed without it will not accept installs. +In catalog mode, a runtime connected to an ObjectStack Cloud account presents its connection credential to the cloud catalog — see [Cloud Connection](/docs/deployment/cloud-connection). + ## Configuration The CLI looks for `objectstack.config.ts` (or `.js`, `.mjs`) in the current directory: diff --git a/content/docs/deployment/cloud-connection.mdx b/content/docs/deployment/cloud-connection.mdx new file mode 100644 index 00000000000..4f2ba8c1e21 --- /dev/null +++ b/content/docs/deployment/cloud-connection.mdx @@ -0,0 +1,227 @@ +--- +title: Cloud Connection — connect a self-hosted server to ObjectStack Cloud +navTitle: Cloud Connection +description: Connect your own ObjectStack server to an ObjectStack Cloud account from Setup → Cloud Connection with a one-time code, then rename or disconnect it. What the server stores, and what disconnecting removes. +--- + +A self-hosted ObjectStack server can be connected to an ObjectStack Cloud +account. You start the connection on your own server, in **Setup → Apps → +Cloud Connection**, and approve it in the cloud account with a one-time code. +Nothing is typed into the server: no id, no key, no password. Under the hood +this is the OAuth 2.0 device authorization grant +([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)), run by the +`@objectstack/cloud-connection` package on your server. + +This page covers the server half of that flow, which is what the framework +decides. The cloud account's own screens are described only as far as the +server determines them: the link and code it shows you, and the answers the +cloud sends back. + +## What connecting gives you [#what-you-get] + +- **A registration in your cloud organization.** When you approve, the cloud + answers with a runtime id, the organization the connection belongs to, the + email of the account that approved it, and the time it was made. The server + keeps them and shows them on its Cloud Connection page. +- **Your organization's own packages in this server's Marketplace.** While the + server is connected, it asks the cloud for your organization's catalog with + its own credential, and it presents the same credential when it fetches a + package from the cloud catalog to install it. Not connected, those requests + go out anonymously and the organization catalog is empty. + +What you do **not** need a connection for: browsing the public marketplace and +installing public packages work on an unconnected server, and air-gapped +installs from a compiled artifact (`os package install ./dist/objectstack.json`) +never call the cloud at all. + +## Before you start [#before-you-start] + +**The Cloud Connection plugin is mounted.** `os start`, `os serve` and `os dev` +mount it for you whenever a control-plane URL resolves, unless your +`objectstack.config.ts` already composes it. The URL is `OS_CLOUD_URL`; unset, +it is `https://cloud.objectos.ai`. Setting `OS_CLOUD_URL` to `off`, `none`, +`local` or `disabled` mounts no cloud surface at all: Setup shows no Cloud +Connection entry and `/api/v1/cloud-connection/*` answers `404`. A host that +composes its plugins by hand adds it in single-environment mode: + +```ts +import { CloudConnectionPlugin, resolveCloudUrl } from '@objectstack/cloud-connection'; + +const cloudUrl = resolveCloudUrl(); // OS_CLOUD_URL; '' when declared off + +// ...in your plugin list: +...(cloudUrl ? [new CloudConnectionPlugin({ singleEnvironment: true, controlPlaneUrl: cloudUrl })] : []), +``` + +To check a running server, request `GET /api/v1/cloud-connection/status`. It +needs no sign-in, and a `200` means the plugin is mounted. + +**The server can reach the cloud.** Every call to the cloud is made by the +server, not by your browser: the code request, the polling, the connection +itself and the disconnect. The server needs outbound HTTPS to the control-plane +URL. Your browser only opens the approval link. + +**The server's working directory is persistent.** The connection is stored in +a file under the server's working directory, +`.objectstack/cloud-connection.json` (see +[What the server stores](#what-the-server-stores)). In the official runtime +image the working directory is `/srv/app`. If that directory lives on the +container's own filesystem, replacing the container loses the connection, and +the server reads as not connected until you connect it again. + +**The right people on each side.** + +- *On the server*, you sign in with a user of this server. The Cloud + Connection page is in the Setup app, which takes the **Setup Access** + permission (`setup.access`); the default platform-administrator and + organization-administrator permission sets carry it. The connect and + disconnect requests themselves check that the caller is signed in to this + server; they do not check Setup Access or any other permission. A session + that the authentication policy is holding (an expired password, or + multi-factor sign-in required but not yet set up) is refused with `403` + until it is fixed. +- *In the cloud*, the approval is made by whoever signs in to the cloud + account behind the link and approves the request. The server records that + account's email as **Approved by**. Which cloud accounts and roles may + approve is decided by the cloud account, not by your server. + +## Connect [#connect] + +1. **Open the page.** Sign in to your server and go to **Setup → Apps → + Cloud Connection**. The header reads *Cloud Connection — Connect this + runtime to an ObjectStack control plane to browse your organization's + private packages and install them here.* The panel shows *Checking + connection…*, then **Not connected** and a **Connect** button. +2. **Click Connect.** The server asks the cloud for a device code and hands + the panel four things: the user code, the approval link, how often to + check (5 seconds unless the cloud says otherwise) and how long the code + lives (600 seconds unless the cloud says otherwise). The approval link + names your server, so the approver can see what they are approving: it + carries `runtime_name` (the server's host name) and `runtime_version`. +3. **The approval page opens.** The panel opens the approval link in a new + window and says *Approve the connection in the window that just opened — + this page updates by itself.* If the browser blocked the window, the panel + says *Waiting for approval in the cloud console…* and shows an **Open the + approval page** button instead. Either way the user code is shown in large + type, with a **Copy** button and a **Cancel** button. +4. **Approve in the cloud account.** Open the link, sign in to the cloud + account, and approve the request whose code matches the code on your + server's page. +5. **The server finishes on its own.** While you approve, the panel asks the + server at that interval (never more often than every 2 seconds) whether the + request is approved, and the server asks the cloud. Once it is, the server completes the connection, stores the + credential it receives, and the panel turns to **Connected to ObjectStack + Cloud**, listing whichever of these the cloud reported: **Runtime** (the + name), **Organization**, **Approved by**, **Runtime ID**, **Environment** + and **Since**. Below the list: *Your organization's private packages now + appear in the Marketplace under “Your organization”.* + +When the flow stops instead, the panel shows a message and a **Try again** +button: + +| The panel says | Why | +|:---|:---| +| *The request expired before it was approved. Start again.* | The code's lifetime ran out, or the cloud answered `expired_token`. | +| *The connection request was denied. Start again.* | The cloud answered `access_denied`. | +| *Device authorization failed: …* | The cloud ended the request with another error, named after the colon. | +| *Sign in to this environment to connect a cloud account.* (or *Sign in to this environment.* while waiting) | Your session on the server has ended. | +| *No cloud control plane configured.* | The plugin is mounted without a control-plane URL. | +| a network error such as *fetch failed* | The server could not reach the cloud. | + +**Cancel** only stops the panel from checking; nothing is sent to the cloud. +The server completes a connection only while the panel is checking, so a +request approved after you cancelled never completes, and an unapproved code +simply expires. + +## What the server stores [#what-the-server-stores] + +The connection lives in one file, `.objectstack/cloud-connection.json` under +the server's working directory, written with mode `0600`: + +| Field | What it is | +|:---|:---| +| `runtimeToken` | The credential the cloud issued for this server. It is the secret: anyone holding it acts as this server towards the cloud. | +| `runtimeId` | The cloud's id for this server. It is kept across disconnects (see [Disconnect](#disconnect)). | +| `controlPlaneUrl` | The cloud the connection was made against. | +| `organizationId`, `accountEmail`, `boundAt` | The organization, the approving account and the time, as the cloud reported them. | + +The credential never reaches the browser: the server keeps it and removes it +from the answer it sends the page. The server reads the file on every cloud +call, so a connection made while the server is running takes effect without a +restart, and so does a disconnect. Keep the file on persistent storage and +protect it like any other secret. + +## Rename [#rename] + +The name your server is known by is its **host name** at the moment you +connect. The server sends it with the connection request, and the **Runtime** +row of the Cloud Connection page shows the name the cloud reports back. + +The server has no rename action: neither the page nor any +`/api/v1/cloud-connection/*` request changes the name. What the server does +is send its host name again every time it connects, together with the stored +runtime id. So to give the server a different name from its side, change the +host name, then [disconnect](#disconnect) and [connect](#connect) again. In a +container the host name is whatever the container runtime assigned, so set an +explicit one before you connect if you want a readable, stable name. Whether +the cloud account also lets you rename the entry directly is decided by the +cloud account. + +The version shown next to the name is `OS_RUNTIME_VERSION` when the deployment +sets it. Unset, the server sends the Cloud Connection plugin's own version +string, which is not your server's release, so set `OS_RUNTIME_VERSION` if +approvers should see the release you run. + +## Disconnect [#disconnect] + +On the connected panel, click **Disconnect**. The server then: + +1. **Asks the cloud to revoke the connection**, presenting the stored + credential. This is best effort: if the cloud cannot be reached, the server + carries on. +2. **Clears its credential either way.** The file is rewritten to hold only + the runtime id: `runtimeToken` is emptied, and `organizationId`, + `accountEmail`, `boundAt` and `controlPlaneUrl` are dropped. With no runtime + id on record, the file is deleted instead. + +The panel then reads **Not connected**, and the Marketplace's organization +catalog is empty again. + +What disconnecting **keeps**: + +- **The runtime id.** Connecting again sends it along, so the cloud can attach + the new connection to the same registration rather than a new one. To start + over as a brand-new server, delete `.objectstack/cloud-connection.json` + after disconnecting. +- **Everything installed.** Disconnecting touches only the connection file. + Packages already installed into this server stay installed and keep working. + +The answer to `POST /api/v1/cloud-connection/unbind` carries `revoked` and +`cleared`. `revoked: false` means the cloud was not reached or refused the +revocation: the server dropped its credential anyway, but the cloud was not +told. The panel does not show this field and reads **Not connected** either +way, so if you disconnect while the cloud is unreachable, check the cloud +account afterwards. + +## Reference [#reference] + +The routes the Cloud Connection page uses, all on the server's own origin: + +| Route | Sign-in | What it does | +|:---|:---|:---| +| `GET /api/v1/cloud-connection/status` | not required | `bound`, `runtimeId`, and `connection`, the connection as the cloud reports it: name, organization, approving account's email, time. `bound` is `true` whenever the server holds a credential; `connection` is `null` when the cloud cannot be reached. | +| `POST /api/v1/cloud-connection/bind/start` | required | Asks the cloud for a device code; answers with `user_code`, `verification_uri`, `verification_uri_complete`, `interval`, `expires_in`. | +| `POST /api/v1/cloud-connection/bind/poll` | required | Body `{ "device_code": "…" }`. Answers `{ "pending": true }` while the cloud waits for approval, and the connection once it is approved. | +| `POST /api/v1/cloud-connection/unbind` | required | Disconnects; answers `{ "revoked", "cleared" }`. | + +The settings that shape the flow are listed in +[Environment variables](/docs/deployment/environment-variables#multi-tenant--cloud-control-plane): +`OS_CLOUD_URL` picks the cloud, `OS_CLI_CLIENT_ID` overrides the OAuth client +id the server presents (default `objectstack-cli`), and `OS_RUNTIME_VERSION` +sets the version it reports. `OS_CLOUD_API_KEY` and `OS_ENVIRONMENT_ID` belong +to cloud-hosted runtimes; a self-hosted server leaves both unset, and a set +`OS_CLOUD_API_KEY` is used instead of the stored credential. + +The Setup app itself, and how plugins add entries to it, is described in +[Setup app](/docs/ui/setup-app). Running the server is covered in +[Self-hosting](/docs/deployment/self-hosting). diff --git a/content/docs/deployment/meta.json b/content/docs/deployment/meta.json index 0274ee0087f..83f927e2ea2 100644 --- a/content/docs/deployment/meta.json +++ b/content/docs/deployment/meta.json @@ -9,6 +9,7 @@ "single-project-mode", "publish-and-preview", "install-modes", + "cloud-connection", "validating-metadata", "cli", "environment-variables", diff --git a/content/docs/protocol/kernel/metadata-service.mdx b/content/docs/protocol/kernel/metadata-service.mdx index 232ec951740..6836be8c398 100644 --- a/content/docs/protocol/kernel/metadata-service.mdx +++ b/content/docs/protocol/kernel/metadata-service.mdx @@ -228,6 +228,8 @@ instance goes through `@objectstack/cloud-connection` (`os package install`). A still-configured `artifact-api` source fails loudly at `start()` rather than silently falling back to the filesystem scan. +Connecting a self-hosted instance to an ObjectStack Cloud account is covered in [Cloud Connection](/docs/deployment/cloud-connection). + ### 2. Persistence Write Gate `MetadataManagerConfigSchema.persistence` is a runtime freeze. The flag defaults to `true`. diff --git a/content/docs/ui/setup-app.mdx b/content/docs/ui/setup-app.mdx index 9e1cfc126a9..d7acf52ec19 100644 --- a/content/docs/ui/setup-app.mdx +++ b/content/docs/ui/setup-app.mdx @@ -48,7 +48,7 @@ anchors are: | Group (anchor id) | Filled by | |:---|:---| | **Overview** (`group_overview`) | System Overview dashboard — `platform-objects` | -| **Apps** (`group_apps`) | Packages — `platform-objects`; marketplace — `cloud-connection` (capability plugin) | +| **Apps** (`group_apps`) | Packages — `platform-objects`; marketplace and [Cloud Connection](/docs/deployment/cloud-connection) — `cloud-connection` (capability plugin) | | **People & Organization** (`group_people_org`) | Users · Organization · Business Units · Teams · Organizations · Invitations — `platform-objects` | | **Access Control** (`group_access_control`) | Positions / Permission Sets — `plugin-security`; Sharing Rules / Record Shares — `plugin-sharing`; API Keys — `platform-objects` | | **Approvals** (`group_approvals`) | Approvals Inbox (the `approvals:inbox` component) · Requests · Action History · Delegations (OOO) — `plugin-approvals` | From 4d2f5c65972e94c388dde8f09aa98adad905914b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 04:27:31 +0000 Subject: [PATCH 2/4] docs(deployment): drop the reserved word from the Cloud Connection page check:role-word (ADR-0090 D3) flagged one new use; the sentence now says which cloud accounts may approve, and with what permissions. Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude --- content/docs/deployment/cloud-connection.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/deployment/cloud-connection.mdx b/content/docs/deployment/cloud-connection.mdx index 4f2ba8c1e21..e422cec64ab 100644 --- a/content/docs/deployment/cloud-connection.mdx +++ b/content/docs/deployment/cloud-connection.mdx @@ -82,8 +82,8 @@ the server reads as not connected until you connect it again. until it is fixed. - *In the cloud*, the approval is made by whoever signs in to the cloud account behind the link and approves the request. The server records that - account's email as **Approved by**. Which cloud accounts and roles may - approve is decided by the cloud account, not by your server. + account's email as **Approved by**. Which cloud accounts may approve, and + with what permissions, is decided by the cloud account, not by your server. ## Connect [#connect] From 08ef85285e6885c7cb988852cea2b47835aec06e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 04:52:47 +0000 Subject: [PATCH 3/4] docs(audit-scope): list the Cloud Connection page in handwritten-docs.json Generated by `node scripts/docs-audit/check-audit-scope.mjs --write`; the one added line is the new content/docs/deployment/cloud-connection.mdx, which check:docs-audit-scope requires for every hand-written page. Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude --- scripts/docs-audit/handwritten-docs.json | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/docs-audit/handwritten-docs.json b/scripts/docs-audit/handwritten-docs.json index 0d8f414e2cb..8a8be41dd6c 100644 --- a/scripts/docs-audit/handwritten-docs.json +++ b/scripts/docs-audit/handwritten-docs.json @@ -92,6 +92,7 @@ "content/docs/data-modeling/validation.mdx", "content/docs/deployment/backup-restore.mdx", "content/docs/deployment/cli.mdx", + "content/docs/deployment/cloud-connection.mdx", "content/docs/deployment/environment-variables.mdx", "content/docs/deployment/index.mdx", "content/docs/deployment/install-modes.mdx", From a6e91355390c017b53d15bcfd6707487eb6f26f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 04:53:16 +0000 Subject: [PATCH 4/4] docs(deployment): name the Cloud Connection defects as defects, not contract The session-only check on connect/disconnect is now a known-defect note pointing at its card, with interim advice for operators; the status row no longer lists the approver's email and organization as fields anonymous callers read, and points at the same card; the OS_RUNTIME_VERSION fallback is named a known defect with its card. #disconnect unchanged. Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude --- content/docs/deployment/cloud-connection.mdx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/content/docs/deployment/cloud-connection.mdx b/content/docs/deployment/cloud-connection.mdx index e422cec64ab..b1d8d22c502 100644 --- a/content/docs/deployment/cloud-connection.mdx +++ b/content/docs/deployment/cloud-connection.mdx @@ -74,9 +74,12 @@ the server reads as not connected until you connect it again. - *On the server*, you sign in with a user of this server. The Cloud Connection page is in the Setup app, which takes the **Setup Access** permission (`setup.access`); the default platform-administrator and - organization-administrator permission sets carry it. The connect and - disconnect requests themselves check that the caller is signed in to this - server; they do not check Setup Access or any other permission. A session + organization-administrator permission sets carry it. A known defect in the + connect and disconnect requests: today these requests check only that the + caller is signed in, not Setup Access + ([#24148](https://github.com/objectstack-ai/objectstack/issues/24148)); + until that is fixed, give sign-in on this server only to people who may + change its cloud connection. A session that the authentication policy is holding (an expired password, or multi-factor sign-in required but not yet set up) is refused with `403` until it is fixed. @@ -170,7 +173,8 @@ cloud account. The version shown next to the name is `OS_RUNTIME_VERSION` when the deployment sets it. Unset, the server sends the Cloud Connection plugin's own version string, which is not your server's release, so set `OS_RUNTIME_VERSION` if -approvers should see the release you run. +approvers should see the release you run. That fallback is a known defect, +tracked in [#24150](https://github.com/objectstack-ai/objectstack/issues/24150). ## Disconnect [#disconnect] @@ -209,7 +213,7 @@ The routes the Cloud Connection page uses, all on the server's own origin: | Route | Sign-in | What it does | |:---|:---|:---| -| `GET /api/v1/cloud-connection/status` | not required | `bound`, `runtimeId`, and `connection`, the connection as the cloud reports it: name, organization, approving account's email, time. `bound` is `true` whenever the server holds a credential; `connection` is `null` when the cloud cannot be reached. | +| `GET /api/v1/cloud-connection/status` | not required | `bound`, `runtimeId`, and `connection`, the connection as the cloud reports it. `bound` is `true` whenever the server holds a credential; `connection` is `null` when the cloud cannot be reached. Narrowing what unprivileged callers see is tracked in [#24148](https://github.com/objectstack-ai/objectstack/issues/24148). | | `POST /api/v1/cloud-connection/bind/start` | required | Asks the cloud for a device code; answers with `user_code`, `verification_uri`, `verification_uri_complete`, `interval`, `expires_in`. | | `POST /api/v1/cloud-connection/bind/poll` | required | Body `{ "device_code": "…" }`. Answers `{ "pending": true }` while the cloud waits for approval, and the connection once it is approved. | | `POST /api/v1/cloud-connection/unbind` | required | Disconnects; answers `{ "revoked", "cleared" }`. |