# Organization Install Links Status: self-host operator guide Owner: platform/self-host Related: `ee/apps/den-api/src/install-links.ts`, `ee/apps/den-api/src/routes/org/install-links.ts`, `ee/apps/den-api/src/desktop-connect-grants.ts`, `packaging/helm/openwork-ee/README.md` ## What users get An organization install link opens a three-step Den page: 1. Download and run the standard OpenWork installer. 2. Return to Den and click **Open OpenWork**. 3. Confirm the exact organization and server in the app, then complete normal organization sign-in. This flow does not build a customer-specific installer or ZIP. Branding and server configuration are applied only after the user confirms the deep link. Possession of the install link never signs a user in or grants workspace access. The guided flow is scoped to Den organization install links. The public `/desktop` and landing-page downloads keep their existing behavior. ## Upgrade checklist > **Default:** Install links are active for every organization on every > deployment. `DEN_INSTALL_LINKS_GATING_ENABLED` is deprecated and inert; > platform admins can turn an org dark from `/admin` as a kill switch. ### Helm 1. Keep `migrations.enabled=true` for the upgrade. The migration Job creates both `install_link` and `desktop_connect_grant`. 2. Keep `config.public.connectLinkMode: exchange` (the default). No key is required. 3. Restart or roll the deployment. No install-link capability toggle is required for a normal self-hosted installation. 4. No hosted-style rollout flag is required: install links are on by default. `DEN_INSTALL_LINKS_GATING_ENABLED` is deprecated and inert; configure `config.public.bootstrapAdminEmails` only if platform admins need `/admin` access to turn an org dark as a kill switch. ### Docker Compose Run the migration once, then restart Den: ```bash docker compose -f packaging/docker/docker-compose.den-dev.yml exec den sh -lc \ "node /app/ee/packages/den-db/dist/scripts/bootstrap.js" ``` If the stack has a custom Compose project name, include the same `-p ` flag. ## Public origins Set `DEN_BASE_URL` to the externally reachable Den web origin, for example `https://openwork.example.com`. Den derives Better Auth, CORS/trusted origins, web-app hosts, API defaults, and MCP resource defaults from that one value. Set `DEN_API_PUBLIC_URL` only as a compatibility override when you publish a separate Den API origin or an externally reachable proxy path, such as `https://openwork.example.com/api/den`. If you publish a separate Den API origin, the install-link exchange and external MCP clients must be able to reach it. Invitation acceptance links use the first non-wildcard trusted origin. In the single-origin setup this is derived from `DEN_BASE_URL`; set `DEN_BETTER_AUTH_TRUSTED_ORIGINS` only when you need extra web origins during a migration. ## Installer delivery Customer-facing planning guidance for this section is published at [`packages/docs/start-here/installer-delivery.mdx`](../packages/docs/start-here/installer-delivery.mdx). This repository page remains authoritative for implementation and security details. Den first validates the organization install token. It then chooses exactly one of these paths: | Deployment | Download behavior | Required client access | |---|---|---| | Internet-connected | Immediate `302` to the exact standard asset under `OPENWORK_INSTALLER_RELEASE_REPO` and `OPENWORK_INSTALLER_RELEASE_TAG`. The organization token is not forwarded. | Den web/API, `github.com`, `release-assets.githubusercontent.com`, and `objects.githubusercontent.com` for legacy or rollback paths. | | Semi-air-gapped | Stream the matching standard installer from `OPENWORK_INSTALLER_ARTIFACTS_DIR`. There is no ZIP or in-memory whole-file buffer. | Den web/API only. | | Fully internal installer delivery | Same mounted-artifact path, with Den web/API and the installer artifact available entirely inside the isolated network. This describes installer bytes only, not full product isolation. | Internal Den web/API only. | The standard filenames use the release tag without a leading `v`: - `openwork-mac-arm64-.dmg` - `openwork-mac-x64-.dmg` - `openwork-win-x64-.exe` - `openwork-linux-x86_64-.AppImage` - `openwork-linux-arm64-.AppImage` There is no first-request GitHub download inside Den, artifact lookup API call, ZIP creation, per-pod cold cache, or different repeated-download path. Every internet-connected request redirects immediately. Every mounted request streams the same provisioned file. With more than one Den API replica, mount the same read-only PVC on every replica. ## Default connection handoff (no key) `DEN_CONNECT_LINK_MODE=exchange` is the default. When the user clicks **Open OpenWork**, Den mints a fresh five-minute bearer code and stores only its SHA-256 hash. The app: 1. posts the code back to the exact HTTPS API origin carried in the deep link to preview the organization and server; 2. shows an explicit confirmation without changing local configuration; and 3. posts the code to the exchange endpoint after confirmation, consuming it exactly once before writing `desktop-bootstrap.json`. The grant is stored in MySQL, not pod memory, so preview and consumption can land on different Kubernetes replicas. A conditional database update makes concurrent exchange attempts single-use. Expired rows are removed while new grants are minted. The web page fetches a new code at the moment the user clicks **Open**, rather than relying on the code loaded before a potentially long installer run. If a code expires or was already used, the user can return to the same Den page and click **Open** again. ## Optional signed handoff Signed handoffs remain an explicit upgrade path. They are disabled unless all of the following are true: - `DEN_CONNECT_LINK_MODE=signed`; - `DEN_CONNECT_LINK_KEY_ID` is configured; - `DEN_CONNECT_LINK_PRIVATE_KEY` contains the dedicated Ed25519 private key; - the matching public key is already embedded in the desktop build. For Helm: ```yaml config: public: connectLinkMode: signed connectLinkKeyId: "owc-2026-07" secret: values: connectLinkPrivateKey: |- -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY----- ``` Generate a pair with `scripts/generate-connect-link-keypair.mjs`. Do not enable signed mode until the production release embeds the corresponding public key. This optional mode changes only the configuration handoff; it still downloads the same standard installer through the same direct or mounted route. ## MDM alternative Customer-facing guidance for this path is published at [`packages/docs/start-here/enterprise-desktop-deployment.mdx`](../packages/docs/start-here/enterprise-desktop-deployment.mdx). Managed deployments can skip the deep-link handoff by deploying a standard binary — typically the enterprise distribution (`openwork-enterprise---.`, published on the `enterprise` release channel), or the public installer — and writing `desktop-bootstrap.json` directly: | OS | Canonical path | |---|---| | Windows | `%LOCALAPPDATA%\openwork\desktop-bootstrap.json` (`%XDG_CONFIG_HOME%\openwork\desktop-bootstrap.json` wins if set) | | macOS/Linux | `$XDG_CONFIG_HOME/openwork/desktop-bootstrap.json`, falling back to `~/.config/openwork/desktop-bootstrap.json` | ```json { "baseUrl": "https://openwork.example.com", "apiBaseUrl": "https://api.openwork.example.com", "requireSignin": true, "writtenAt": "2026-07-14T12:00:00.000Z" } ``` Current builds still read the older `~/.config/openwork` path for compatibility. When both files exist, the valid configuration with the newest `writtenAt` wins. ## Security properties - Install-link tokens and exchange codes are stored only as SHA-256 hashes. - Connection codes expire after five minutes and are consumed exactly once in MySQL across all Den API replicas. - The desktop requires HTTPS outside explicit development loopback mode and refuses redirects during preview and exchange. - Returned claims must identify the same API origin carried by the deep link. - The app displays the exact organization and server before changing local configuration; cancel and every validation failure leave it untouched. - Branding URLs from the keyless exchange are not loaded before confirmation. - The standard release installer bytes are never modified, preserving normal macOS and Windows signature verification. ## Troubleshooting | Symptom | Fix | |---|---| | Download redirects to GitHub | Expected for internet-connected deployments. Allow `github.com` and its release-asset redirect/CDN host on the user's network. | | Download returns `404` | The install token is invalid, expired, or revoked. Ask an org member for the current install page link. | | Mounted download fails | Verify the filename exactly matches the configured release tag and every Den replica mounts the same readable PVC path. | | **Open OpenWork** cannot prepare a connection | Verify the browser can reach Den API and `DEN_API_PUBLIC_URL` is the correct public HTTPS origin. | | The app refuses an expired or used link | Return to the same Den install page and click **Open OpenWork** again to mint a fresh code. | | Signed mode reports an unknown key | Return to `exchange` mode, or ship a desktop build containing the matching public key before re-enabling `signed`. | | Install links point at the wrong web host | Correct `BETTER_AUTH_URL` and `DEN_BETTER_AUTH_TRUSTED_ORIGINS`, then restart Den. |