> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0281 — Desktop surfaces: targets per app, desktop modes and overlay styles

> Accepted · 2026-10-22

> [!NOTE]
>
> Extends [0249](/adr/0249-one-app-three-targets-web-pwa-and-electron) (one app, several targets),
> [0250](/adr/0250-stackra-platform-capability-ports-drivers-and-gate) (capability ports in
> `@stackra/platform`) and
> [0251](/adr/0251-desktop-bridge-generated-from-desktop-action-and-electron-hardening) (the
> Electron hardening baseline). The tray keep-alive default of
> [0254](/adr/0254-desktop-release-signing-update-feed-channels-and-version-policy) stands.

> [!NOTE]
>
> Amends [0126](/adr/0126-member-shapes-and-scaffold-templates) (its 2026-10-21 amendment, for app
> members, and the app and service member shapes) and
> [0146](/adr/0146-package-configuration-typescript-layers-and-secret-references) decision 1 (CF1):
> an app's config files under `src/config/` are named per fact or topic, not per package, and
> `stackra.config.ts` is a required file at the root of every app and service member.

## Context

ADR 0249 builds web, PWA and Electron desktop from one app member, but does not say how an app picks its targets or which
window surfaces the desktop target offers. A desktop app can be a dock app, a menu-bar app or both, and can show floating
overlays opened by a system-wide shortcut. A browser tab or an installed PWA can do none of the tray, global-shortcut or
always-on-top work, and config must not pretend otherwise. The owner decided targets, desktop modes and two overlay styles
in chat on 2026-10-22.

## Decision

1. **One member, one codebase, listed targets.** An app member declares its targets in `stackra.config.ts` at the
   member root, which holds the one app definition per member: `targets: [Target.Web, Target.Pwa, Target.Desktop]`.
   `stackra gen` builds only the listed targets. There is no second codebase per target (ADR 0249).
   * **Required root file.** `stackra.config.ts` joins the app and service member shapes of ADR 0126 as a required file
     at the member root, and `pnpm new` creates it.
   * **Config files by topic.** Following the owner's "one fact, one file" decision of 2026-10-21, an app's config files
     sit under `src/config/` and are named by fact or topic, not by package: `desktop.config.ts`, `theming.config.ts`,
     `consent.config.ts` and so on. This amends ADR 0146 decision 1 (CF1, `<pkg>.config.ts`) and the 2026-10-21
     amendment of ADR 0126 (decision 1, `src/config/<name>.config.ts`) for app members. Desktop options live in
     `src/config/desktop.config.ts`.

2. **Capability matrix.** What each target can offer:

   | Surface | Web | PWA | Desktop |
   | - | - | - | - |
   | Normal window | browser tab | standalone window | window |
   | Dock icon | no | yes | yes, except in `Tray` mode |
   | Tray or menu-bar icon | no | no | yes |
   | System-wide shortcuts | no | no | yes |
   | Floating always-on-top overlays | no | no | yes |
   | App shortcuts, share target, badges | no | yes | yes |
   | Offline | storage + mutation queue | the same, plus the service worker | SQLite, or the `fs` driver until it lands |

   Offline follows ADR 0252 (offline on every client). Web uses the `@stackra/storage` web drivers (IndexedDB) and the
   offline mutation queue. PWA has the same, plus the service worker for the app shell and background sync (ADR 0255).
   Desktop uses SQLite through the `./sql` entry of `@stackra/storage` (ADR 0256 Phase 2), and the `fs` driver until that
   lands.

   Config never claims a capability its targets lack. Validation rejects a desktop-only option (`mode`, `overlays`, a
   global shortcut, the tray) when `Target.Desktop` is not in `targets`.

3. **Desktop mode.** `desktop.config.ts` sets `mode` to one `DesktopMode`:
   * `Window`: a dock app with no tray icon.
   * `Tray`: a menu-bar app only. There is no dock or taskbar icon, and the window opens from the tray icon.
   * `Hybrid`: a window and a tray icon. Closing the window keeps the app in the tray only when the user has turned
     that on; it is off by default (ADR 0254, tray keep-alive).

4. **Named overlays.** `desktop.config.ts` declares named `overlays`. Each has a `style` (`OverlayStyle`):

   * `Launcher`: a wide prompt bar centred on the active display that grows downward with results (Claude style). It
     opens by global shortcut or tray menu and closes on Esc or blur. Its width is fixed and its height follows the
     content.
   * `Panel`: a card docked to a screen edge or under the tray icon (Gemini style). It opens by shortcut, tray menu or an
     in-app button and closes by its button. It can be pinned, is resizable, and remembers its size and position.

   Options shared by both styles: `route`; `shortcut` (user-rebindable, registered by the main process only, decision 6);
   `alwaysOnTop`; the behaviour on blur; a vibrancy or blur background (opaque where the OS has none); `anchor`
   (`OverlayAnchor`: `ScreenCenter`, `TrayIcon`, `EdgeRight`, `EdgeLeft`, `Cursor`); and the displays the overlay may
   open on.

5. **Two shells, no second implementation.** `@stackra/platform/react` ships `LauncherOverlay` and `PanelOverlay`. Any
   route renders inside the configured shell, and the same route also works in the main window. A surface never gets its
   own copy of a screen.

6. **Global shortcuts and screen capture.** Global shortcuts are registered by the Electron main process only, never by
   the renderer. Every shortcut is listed in config, checked for conflicts at startup, and can be rebound or disabled by
   the user in settings. Screen capture (`display-capture`) is a permission that is off by default; the renderer stays
   inside the hardening baseline of ADR 0251.

7. **Defaults.**
   * The console app: `Web`, `Pwa` and `Desktop`, desktop mode `Hybrid`, with a `quick` overlay of style `Launcher` on
     `CommandOrControl+Shift+Space` and an `assistant` overlay of style `Panel` anchored `EdgeRight`.
   * A web app: `Web` and `Pwa` only.

8. **Where the enums live.** Each enum is a const object plus a same-named union type in a `<name>.enum.ts` file (ADR
   0128\).
   * `Target` lives in `@stackra/contracts`. `stackra.config.ts` is read by `stackra gen`, the CLI and the scaffolder
     (`pnpm new … --targets`, ADR 0249) as well as by `@stackra/platform`, so it is a shared primitive under ADR 0117
     C1. Placing it in `@stackra/platform` would make the build tools depend on the package that holds the Electron and
     Expo drivers.
   * `DesktopMode`, `OverlayStyle` and `OverlayAnchor` live in `@stackra/platform`. Only `desktop.config.ts` and the
     platform shell read them, the same way each package owns the vocabulary of its own config (ADR 0117 decision 3, the membership test of ADR 0015; ADR 0146).

## Consequences

* `pnpm new` writes `stackra.config.ts` for app and service members, and the scaffold test checks it.
* An app chooses its surfaces in two config files, and a feature written once runs in a tab, a PWA, the desktop window and
  an overlay.
* A PWA config that asks for a tray or a global shortcut fails validation instead of shipping a silent no-op.
* `@stackra/platform` grows a tray port, a global-shortcut port, an overlay window manager and the two React shells; the
  desktop drivers carry the per-OS differences (dock hiding, vibrancy, display placement).
* Settings gains a shortcuts page (rebind and disable) and a screen-capture permission toggle.
* Linear: the overlay shells, the tray and the shortcut work land in the `@stackra/platform` wave, not in a separate
  project.

## Enforcement

* planned — the `stackra.config.ts` and `desktop.config.ts` schemas in `stackra gen` reject desktop-only options when
  `Target.Desktop` is not a target, and gen emits only the listed targets.
* planned — an oxlint boundary rule allows Electron `globalShortcut` and `Tray` imports only in the desktop main-process
  entry; the renderer cannot reach them under ADR 0251 (context isolation, sandbox, generated IPC allow-list).
* planned — a desktop driver test registers every configured shortcut at startup and fails on a conflict; a test checks
  that the permission handler denies `display-capture` until the user grants it.
* planned — a `@stackra/platform` test renders one route in the main window, `LauncherOverlay` and `PanelOverlay`.

## Supersedes and sources

Supersedes: none. Amends: [0126](/adr/0126-member-shapes-and-scaffold-templates) (its 2026-10-21 amendment for app
members, and the app and service member shapes),
[0146](/adr/0146-package-configuration-typescript-layers-and-secret-references) decision 1 (CF1). Extends: [0249](/adr/0249-one-app-three-targets-web-pwa-and-electron),
[0250](/adr/0250-stackra-platform-capability-ports-drivers-and-gate),
[0251](/adr/0251-desktop-bridge-generated-from-desktop-action-and-electron-hardening). Consistent with:
[0254](/adr/0254-desktop-release-signing-update-feed-channels-and-version-policy) (tray keep-alive opt-in),
[0252](/adr/0252-offline-on-every-client-completing-the-mutation-queue) (offline on every client),
[0255](/adr/0255-pwa-service-worker-scope-and-push-per-platform) (PWA offline),
[0256](/adr/0256-client-storage-one-stackra-storage-package) (desktop SQLite through the `./sql` entry, `fs` driver until then). Sources: owner decision in chat,
2026-10-22 (STA-292).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.