npm package: @gimlet-io/backstage-plugin-capacitor
Description: Embeds the **Capacitor Kubernetes Dashboard** inside Backstage via an iframe.
Latest version: 0.3.4

# Backstage Capacitor Plugin

Embeds the **Capacitor Kubernetes Dashboard** inside Backstage via an iframe.

The iframe loads from **`/api/capacitor/`**, which is served by our **custom Capacitor backend proxy plugin** (not the built-in Backstage proxy-backend config). That backend plugin reverse-proxies your standalone Capacitor service (including websockets).

---

## Install (frontend)

Add the plugin dependency to your Backstage app:

```sh
# from your Backstage repo root
yarn add @gimlet-io/backstage-plugin-capacitor --cwd packages/app
```

Add the route in `packages/app/src/App.tsx`:

```tsx
import { CapacitorPage } from '@gimlet-io/backstage-plugin-capacitor';

// inside <FlatRoutes>:
<Route path="/capacitor" element={<CapacitorPage />} />
```

Add a sidebar item in `packages/app/src/components/Root/Root.tsx`:

```tsx
import DashboardIcon from '@material-ui/icons/Dashboard';

// inside your <SidebarGroup>:
<SidebarItem icon={DashboardIcon} to="capacitor" text="Capacitor" />
```

---

## Backend setup (required)

This frontend plugin expects the **Capacitor backend proxy module** to be installed in your Backstage backend. It serves:

- `GET /api/capacitor/*` (reverse proxy to the standalone service)
- websocket upgrades under `/api/capacitor/*`
- `GET /api/capacitor/.backstage/auth/v1/cookie` (Backstage auth “cookie mint” endpoint used by the page before rendering the iframe)

The backend proxy plugin is shipped by this npm package at:

- `@gimlet-io/backstage-plugin-capacitor/backend`

### Minimal wiring example

1) Add the dependency to your Backstage backend package:

```sh
# from your Backstage repo root
yarn add @gimlet-io/backstage-plugin-capacitor --cwd packages/backend
```

2) Register it in `packages/backend/src/index.ts`:

```ts
backend.add(import('@gimlet-io/backstage-plugin-capacitor/backend'));
```

---

## Configuration

Add the proxy target to your Backstage `app-config.yaml`:

```yaml
# Capacitor Kubernetes Dashboard
capacitor:
  proxy:
    # Where the standalone service actually runs
    target: http://localhost:8181
```

### Proxy auth modes

The `/api/capacitor/*` backend proxy has **two modes**:

- **Unauthenticated mode (default)**:
  - **When**: neither `CAPACITOR_PROXY_SHARED_SECRET` nor `capacitor.proxy.sharedSecret` is set
  - **Backstage route policy**: `allow: 'unauthenticated'` (set automatically by the proxy)
  - **Behavior**: requests are proxied to `capacitor.proxy.target` without identity forwarding (no `X-Onurl-*` headers)
  - **Upstream requirement**: the standalone service must allow unauthenticated access (e.g. `AUTH=noauth`)

- **Authenticated / trusted-proxy mode**:
  - **When**: either `CAPACITOR_PROXY_SHARED_SECRET` **or** `capacitor.proxy.sharedSecret` is set (non-empty)
  - **Backstage route policy**: `allow: 'user-cookie'` (set automatically by the proxy)
  - **Behavior**: Backstage signs and forwards the current user identity to the upstream via `X-Onurl-*` headers
  - **Upstream requirement**: start the standalone service with `AUTH=trusted_proxy` and the same shared secret

**Config precedence**: `CAPACITOR_PROXY_SHARED_SECRET` (env) is used if set; otherwise `capacitor.proxy.sharedSecret` (app-config) is used.

Add the shared secret (optional) in `app-config.yaml`:

```yaml
capacitor:
  proxy:
    sharedSecret: ${CAPACITOR_PROXY_SHARED_SECRET}
```

### Backstage backend auth key (recommended)

If you run the proxy in **authenticated / user-cookie** mode (i.e. you set `CAPACITOR_PROXY_SHARED_SECRET` / `capacitor.proxy.sharedSecret`), Backstage will mint a `backstage-auth` cookie for the `/api/capacitor/*` routes.

For local/dev setups, make sure your Backstage backend has a **stable** `backend.auth.keys` secret; otherwise you may see warnings like `ERR_JWKS_NO_MATCHING_KEY` after restarts and the Capacitor iframe/proxy auth flow can fail until cookies are cleared.

Add this to your Backstage `app-config.yaml`:

```yaml
backend:
  auth:
    keys:
      - secret: ${BACKEND_SECRET}
```

And set `BACKEND_SECRET` to a stable value (same value across restarts).

### Development notes (ports & `/api` proxy)

When you run Backstage locally using `yarn start` / `npm run start`, Backstage typically runs **two servers**:

- **Frontend (port 3000)**: the UI dev server (what your browser loads)
- **Backend (port 7007)**: the Backstage backend (serves `/api/*`, including this plugin’s `/api/capacitor/*` route)

This plugin’s UI renders an iframe pointing at **`/api/capacitor/`** (same origin as the frontend). In dev, that means the browser requests: `http://localhost:3000/api/capacitor/...`

Without a frontend dev-server proxy, those requests **never reach the backend** and will instead hit the frontend server (often returning the Backstage HTML app again — “Backstage inside Backstage”).

To forward `/api/*` from the frontend dev server to the backend dev server, add this to **your Backstage app** at `packages/app/package.json`:

```json
{
  "proxy": [
    {
      "context": ["/api"],
      "target": "http://localhost:7007",
      "changeOrigin": true,
      "secure": false,
      "ws": true
    }
  ]
}
```

In production you usually don’t need this setting, because your ingress/reverse-proxy (or deployment) routes `/api/*` to the backend.

### Note on direct access

If authenticated mode is enabled, hitting `http://localhost:3000/api/capacitor/` directly in a fresh browser session can 401 until the cookie is minted. Visiting the Backstage page at `/capacitor` mints it automatically via: `GET /api/capacitor/.backstage/auth/v1/cookie`

If you still see `ERR_JWKS_NO_MATCHING_KEY`, clear the `backstage-auth` cookie for `localhost` (or open an incognito window) and refresh.